Skip to content

Commit c898427

Browse files
committed
Merge branch 'release-candidate' into release
2 parents b3a1f93 + 592e84f commit c898427

File tree

5 files changed

+243
-17
lines changed

5 files changed

+243
-17
lines changed

README.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ uFBT uses your system's Python for running bootstrap code. Minimal supported ver
1515

1616
On first run, uFBT will download and install required SDK components from `release` update channel of official firmware. For more information on how to switch to a different version of the SDK, see [Managing the SDK](#managing-the-sdk) section.
1717

18+
### Using pyenv
19+
20+
If you are using `pyenv` to manage python versions, after installation you may need to run `pyenv rehash` to generate [shim](https://github.com/pyenv/pyenv#understanding-shims) for `ufbt` command.
21+
1822
## Usage
1923

2024
### Building & running your application
@@ -27,9 +31,9 @@ To see other available commands and options, run `ufbt -h`.
2731

2832
### Debugging
2933

30-
In order to debug your application, you need to be running the firmware distributed alongside with current SDK version. You can flash it to your Flipper using `ufbt flash` (over ST-Link), `ufbt flash_usb` (over USB) or `ufbt flash_blackmagic` (using Wi-Fi dev board running Blackmagic firmware).
34+
In order to debug your application, you need to be running the firmware distributed alongside with current SDK version. You can flash it to your Flipper using `ufbt flash` (using a supported SWD probe), `ufbt flash_usb` (over USB).
3135

32-
You can attach to running firmware using `ufbt debug` (for ST-Link) or `ufbt blackmagic` (for Wi-Fi dev board).
36+
For other flashing and debugging options, see `ufbt -h`.
3337

3438
### VSCode integration
3539

@@ -44,7 +48,8 @@ Application manifests are explained in the [FBT documentation](https://github.co
4448
### Other
4549

4650
* `ufbt cli` starts a CLI session with the device;
47-
* `ufbt lint`, `ufbt format` run clang-format on application's sources.
51+
* `ufbt lint`, `ufbt format` run clang-format on application's sources;
52+
* You can temporarily add toolchain binaries (compiler, linter, OpenOCD and others) to your PATH. See `ufbt --help` for more information.
4853

4954
## Managing the SDK
5055

@@ -57,8 +62,15 @@ To update the SDK, run `ufbt update`. This will download and install all require
5762
- uFBT can also download and update the SDK from any **fixed URL**. To do this, run `ufbt update --url=<url>`.
5863
- To use a **local copy** of the SDK, run `ufbt update --local=<path>`. This will use the SDK located in `<path>` instead of downloading it. Useful for testing local builds of the SDK.
5964

60-
uFBT stores its state in `.ufbt` subfolder in your home directory. You can override this location by setting `UFBT_HOME` environment variable.
65+
### Global and per-project SDK management
66+
67+
By default, uFBT stores its state - SDK and toolchain - in `.ufbt` subfolder of your home directory. You can override this location by setting `UFBT_HOME` environment variable.
68+
69+
uFBT also supports dotenv (`.env`) files, containing environment variable overrides for the project in current directory. Most commonly, you will want to use this to override the default state directory to a local one, so that your project could use a specific version and/or hardware target of the SDK.
6170

71+
You can enable dotenv mode for current directory by running `ufbt dotenv_create`. This will create `.env` file in current directory with default values, linking SDK state to `.ufbt` subfolder in current directory, and creating a symlink for toolchain to `.ufbt/toolchain` in your home directory. You can then edit `.env` file to further customize the environment.
72+
73+
You can also specify additional options when creating the `.env` file. See `ufbt dotenv_create --help` for more information.
6274

6375
### ufbt-bootstrap
6476

@@ -69,3 +81,9 @@ Updating the SDK is handled by uFBT component called _bootstrap_. It has a dedic
6981
If something goes wrong and uFBT state becomes corrupted, you can reset it by running `ufbt clean`. If that doesn't work, you can try removing `.ufbt` subfolder manually from your home folder.
7082

7183
`ufbt-bootstrap` and SDK-related `ufbt` subcommands accept `--verbose` option that will print additional debug information.
84+
85+
## Contributing
86+
87+
uFBT is a small tool and does not contain the actual implementation of build system, project templates or toolchain. It functions as a downloader and manager of SDK components that are packaged [alongside with Flipper firmware](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/scripts/ufbt).
88+
89+
Issues and pull requests regarding `ufbt-bootstrap` features like SDK management should be reported to this project, and the rest - related to actual application development - to [Flipper firmware repo](https://github.com/flipperdevices/flipperzero-firmware/issues).

VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.2.4.3
1+
0.2.5

test.py

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import json
22
import subprocess
33
import unittest
4+
from pathlib import Path
5+
from tempfile import TemporaryDirectory
46

57

68
# ufbt invokation & json status output
7-
def ufbt_status() -> dict:
9+
def ufbt_status(cwd=None) -> dict:
810
# Call "ufbt status --json" and return the parsed json
911
try:
10-
status = subprocess.check_output(["ufbt", "status", "--json"])
12+
status = subprocess.check_output(["ufbt", "status", "--json"], cwd=cwd)
1113
except subprocess.CalledProcessError as e:
1214
status = e.output
1315
return json.loads(status)
1416

1517

16-
def ufbt_exec(args):
18+
def ufbt_exec(args, cwd=None):
1719
# Call "ufbt" with the given args and return the parsed json
18-
return subprocess.check_output(["ufbt"] + args)
20+
return subprocess.check_output(["ufbt"] + args, cwd=cwd)
1921

2022

2123
# Test initial deployment
@@ -84,3 +86,74 @@ def test_target_mode_switches(self):
8486
ufbt_exec(["update"])
8587
status = ufbt_status()
8688
self.assertEqual(previous_status, status)
89+
90+
def test_dotenv_basic(self):
91+
ufbt_exec(["clean"])
92+
status = ufbt_status()
93+
self.assertEqual(status.get("error"), "SDK is not deployed")
94+
95+
ufbt_exec(["update", "-t", "f7"])
96+
status = ufbt_status()
97+
self.assertEqual(status.get("target"), "f7")
98+
self.assertEqual(status.get("mode"), "channel")
99+
self.assertEqual(status.get("details", {}).get("channel"), "release")
100+
101+
with TemporaryDirectory() as tmpdir:
102+
local_dir = Path(tmpdir) / "local_env"
103+
local_dir.mkdir(exist_ok=False)
104+
105+
ufbt_exec(["dotenv_create"], cwd=local_dir)
106+
status = ufbt_status(cwd=local_dir)
107+
self.assertEqual(status.get("target"), None)
108+
self.assertIn(
109+
str(local_dir.absolute()), str(Path(status.get("state_dir")).absolute())
110+
)
111+
self.assertEqual(status.get("error"), "SDK is not deployed")
112+
113+
ufbt_exec(["update", "-b", "dev"], cwd=local_dir)
114+
status = ufbt_status(cwd=local_dir)
115+
self.assertEqual(status.get("target"), "f7")
116+
self.assertEqual(status.get("mode"), "branch")
117+
self.assertEqual(status.get("details", {}).get("branch", ""), "dev")
118+
119+
status = ufbt_status()
120+
self.assertEqual(status.get("target"), "f7")
121+
self.assertEqual(status.get("mode"), "channel")
122+
123+
def test_dotenv_notoolchain(self):
124+
with TemporaryDirectory() as tmpdir:
125+
local_dir = Path(tmpdir) / "local_env"
126+
local_dir.mkdir(exist_ok=False)
127+
128+
ufbt_exec(["dotenv_create"], cwd=local_dir)
129+
status = ufbt_status(cwd=local_dir)
130+
131+
toolchain_path_local = status.get("toolchain_dir", "")
132+
self.assertTrue(Path(toolchain_path_local).is_symlink())
133+
134+
# 2nd env
135+
local_dir2 = Path(tmpdir) / "local_env2"
136+
local_dir2.mkdir(exist_ok=False)
137+
138+
ufbt_exec(["dotenv_create", "--no-link-toolchain"], cwd=local_dir2)
139+
status = ufbt_status(cwd=local_dir2)
140+
141+
toolchain_path_local2 = status.get("toolchain_dir", "")
142+
self.assertFalse(Path(toolchain_path_local2).exists())
143+
144+
def test_path_with_spaces(self):
145+
ufbt_exec(["clean"])
146+
status = ufbt_status()
147+
self.assertEqual(status.get("error"), "SDK is not deployed")
148+
149+
with TemporaryDirectory() as tmpdir:
150+
local_dir = Path(tmpdir) / "path with spaces"
151+
local_dir.mkdir(exist_ok=False)
152+
153+
ufbt_exec(["dotenv_create"], cwd=local_dir)
154+
ufbt_exec(["update"], cwd=local_dir)
155+
status = ufbt_status(cwd=local_dir)
156+
self.assertNotIn("error", status)
157+
158+
ufbt_exec(["create", "APPID=myapp"], cwd=local_dir)
159+
ufbt_exec(["faps"], cwd=local_dir)

ufbt/__init__.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,50 @@
2323
import sys
2424

2525
from .bootstrap import (
26+
DEFAULT_UFBT_HOME,
27+
ENV_FILE_NAME,
2628
bootstrap_cli,
2729
bootstrap_subcommands,
2830
get_ufbt_package_version,
29-
DEFAULT_UFBT_HOME,
3031
)
3132

3233
__version__ = get_ufbt_package_version()
3334

3435

36+
def _load_env_file(env_file):
37+
"""
38+
Minimalistic implementation of env file parser.
39+
Only supports lines in format `KEY=VALUE`.
40+
Ignores comments (lines starting with #) and empty lines.
41+
"""
42+
if not os.path.exists(env_file):
43+
return {}
44+
env_vars = {}
45+
with open(env_file) as f:
46+
for line in f:
47+
line = line.strip()
48+
if not line or line.startswith("#"):
49+
continue
50+
key, value = line.split("=", 1)
51+
env_vars[key] = value
52+
return env_vars
53+
54+
3555
def ufbt_cli():
56+
# load environment variables from .env file in current directory
57+
try:
58+
env_vars = _load_env_file(ENV_FILE_NAME)
59+
if env_vars:
60+
os.environ.update(env_vars)
61+
except Exception as e:
62+
print(f"Failed to load environment variables from {ENV_FILE_NAME}: {e}")
63+
return 2
64+
3665
if not os.environ.get("UFBT_HOME"):
3766
os.environ["UFBT_HOME"] = DEFAULT_UFBT_HOME
67+
68+
os.environ["UFBT_HOME"] = os.path.abspath(os.environ["UFBT_HOME"])
69+
3870
# ufbt impl uses UFBT_STATE_DIR internally, not UFBT_HOME
3971
os.environ["UFBT_STATE_DIR"] = os.environ["UFBT_HOME"]
4072
if not os.environ.get("FBT_TOOLCHAIN_PATH"):

ufbt/bootstrap.py

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import json
2323
import logging
2424
import os
25+
import platform
2526
import re
2627
import shutil
2728
import sys
@@ -38,6 +39,8 @@
3839

3940
log = logging.getLogger(__name__)
4041
DEFAULT_UFBT_HOME = os.path.expanduser("~/.ufbt")
42+
ENV_FILE_NAME = ".env"
43+
STATE_DIR_TOOLCHAIN_SUBDIR = "toolchain"
4144

4245

4346
def get_ufbt_package_version():
@@ -493,14 +496,19 @@ def create_for_task(task: SdkDeployTask, download_dir: str) -> BaseSdkLoader:
493496
class UfbtSdkDeployer:
494497
UFBT_STATE_FILE_NAME = "ufbt_state.json"
495498

496-
def __init__(self, ufbt_state_dir: str):
499+
def __init__(self, ufbt_state_dir: str, toolchain_dir: str = None):
497500
self.ufbt_state_dir = Path(ufbt_state_dir)
498501
self.download_dir = self.ufbt_state_dir / "download"
499502
self.current_sdk_dir = self.ufbt_state_dir / "current"
500-
self.toolchain_dir = (
501-
Path(os.environ.get("FBT_TOOLCHAIN_PATH", self.ufbt_state_dir.absolute()))
502-
/ "toolchain"
503-
)
503+
if toolchain_dir:
504+
self.toolchain_dir = self.ufbt_state_dir / toolchain_dir
505+
else:
506+
self.toolchain_dir = (
507+
Path(
508+
os.environ.get("FBT_TOOLCHAIN_PATH", self.ufbt_state_dir.absolute())
509+
)
510+
/ STATE_DIR_TOOLCHAIN_SUBDIR
511+
)
504512
self.state_file = self.current_sdk_dir / self.UFBT_STATE_FILE_NAME
505513

506514
def get_previous_task(self) -> Optional[SdkDeployTask]:
@@ -582,6 +590,9 @@ def __init__(self):
582590
super().__init__(self.COMMAND, "Update uFBT SDK")
583591

584592
def _add_arguments(self, parser: argparse.ArgumentParser) -> None:
593+
parser.description = """Update uFBT SDK. By default uses the last used target and mode.
594+
Otherwise deploys latest release."""
595+
585596
parser.add_argument(
586597
"--hw-target",
587598
"-t",
@@ -611,6 +622,8 @@ def __init__(self):
611622
super().__init__(self.COMMAND, "Clean uFBT SDK state")
612623

613624
def _add_arguments(self, parser: argparse.ArgumentParser):
625+
parser.description = """Clean up uFBT internal state. By default cleans current SDK state.
626+
For cleaning app build artifacts, use 'ufbt -c' instead."""
614627
parser.add_argument(
615628
"--downloads",
616629
help="Clean downloads",
@@ -662,6 +675,8 @@ def __init__(self):
662675
super().__init__(self.COMMAND, "Show uFBT SDK status")
663676

664677
def _add_arguments(self, parser: argparse.ArgumentParser) -> None:
678+
parser.description = """Show uFBT status - deployment paths and SDK version."""
679+
665680
parser.add_argument(
666681
"--json",
667682
help="Print status in JSON format",
@@ -702,6 +717,7 @@ def _func(self, args) -> int:
702717
else:
703718
state_data.update({"error": "SDK is not deployed"})
704719

720+
skip_error_message = False
705721
if key := args.status_key:
706722
if key not in state_data:
707723
log.error(f"Unknown status key {key}")
@@ -714,13 +730,100 @@ def _func(self, args) -> int:
714730
if args.json:
715731
print(json.dumps(state_data))
716732
else:
733+
skip_error_message = True
717734
for key, value in state_data.items():
718735
log.info(f"{self.STATUS_FIELDS[key]:<15} {value}")
719736

720-
return 1 if state_data.get("error") else 0
737+
if state_data.get("error"):
738+
if not skip_error_message:
739+
log.error("Status error: {}".format(state_data.get("error")))
740+
return 1
741+
return 0
742+
743+
744+
class LocalEnvSubcommand(CliSubcommand):
745+
COMMAND = "dotenv_create"
746+
747+
def __init__(self):
748+
super().__init__(self.COMMAND, "Create a local environment for uFBT")
749+
750+
def _add_arguments(self, parser: argparse.ArgumentParser) -> None:
751+
parser.description = f"""Create a dotenv ({ENV_FILE_NAME}) file in current directory with environment variables for uFBT.
752+
Designed for per-project SDK management.
753+
If {ENV_FILE_NAME} file already exists, this command will refuse to overwrite it.
754+
"""
755+
parser.add_argument(
756+
"--state-dir",
757+
help="Directory to create the local environment in. Defaults to '.ufbt'.",
758+
default=".ufbt",
759+
)
721760

761+
parser.add_argument(
762+
"--no-link-toolchain",
763+
help="Don't link toolchain directory to the local environment and create a local copy",
764+
action="store_true",
765+
default=False,
766+
)
722767

723-
bootstrap_subcommand_classes = (UpdateSubcommand, CleanSubcommand, StatusSubcommand)
768+
@staticmethod
769+
def _link_dir(target_path, source_path):
770+
log.info(f"Linking {target_path=} to {source_path=}")
771+
if os.path.lexists(target_path) or os.path.exists(target_path):
772+
os.unlink(target_path)
773+
if platform.system() == "Windows":
774+
# Crete junction - does not require admin rights
775+
import _winapi
776+
777+
if not os.path.isdir(source_path):
778+
raise ValueError(f"Source path {source_path} is not a directory")
779+
780+
if not os.path.exists(target_path):
781+
_winapi.CreateJunction(source_path, target_path)
782+
else:
783+
os.symlink(source_path, target_path)
784+
785+
def _func(self, args) -> int:
786+
if os.path.exists(ENV_FILE_NAME):
787+
log.error(
788+
f"File {ENV_FILE_NAME} already exists, refusing to overwrite. Please remove or update it manually."
789+
)
790+
return 1
791+
792+
env_sdk_deployer = UfbtSdkDeployer(args.state_dir, STATE_DIR_TOOLCHAIN_SUBDIR)
793+
# Will extract toolchain dir from env
794+
default_sdk_deployer = UfbtSdkDeployer(args.ufbt_home)
795+
796+
env_sdk_deployer.ufbt_state_dir.mkdir(parents=True, exist_ok=True)
797+
if args.no_link_toolchain:
798+
log.info("Skipping toolchain directory linking")
799+
else:
800+
env_sdk_deployer.ufbt_state_dir.mkdir(parents=True, exist_ok=True)
801+
default_sdk_deployer.toolchain_dir.mkdir(parents=True, exist_ok=True)
802+
self._link_dir(
803+
str(env_sdk_deployer.toolchain_dir.absolute()),
804+
str(default_sdk_deployer.toolchain_dir.absolute()),
805+
)
806+
log.info("To use a local copy, specify --no-link-toolchain")
807+
808+
env_vars = {
809+
"UFBT_HOME": args.state_dir,
810+
# "TOOLCHAIN_PATH": str(env_sdk_deployer.toolchain_dir.absolute()),
811+
}
812+
813+
with open(ENV_FILE_NAME, "wt") as f:
814+
for key, value in env_vars.items():
815+
f.write(f"{key}={value}\n")
816+
817+
log.info(f"Created {ENV_FILE_NAME} file in {os.getcwd()}")
818+
return 0
819+
820+
821+
bootstrap_subcommand_classes = (
822+
UpdateSubcommand,
823+
CleanSubcommand,
824+
StatusSubcommand,
825+
LocalEnvSubcommand,
826+
)
724827

725828
bootstrap_subcommands = (
726829
subcommand_cls.COMMAND for subcommand_cls in bootstrap_subcommand_classes

0 commit comments

Comments
 (0)