Skip to content

Commit

Permalink
Merge branch 'release-candidate' into release
Browse files Browse the repository at this point in the history
  • Loading branch information
hedger committed Dec 22, 2023
2 parents b3a1f93 + 592e84f commit c898427
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 17 deletions.
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ uFBT uses your system's Python for running bootstrap code. Minimal supported ver

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.

### Using pyenv

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.

## Usage

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

### Debugging

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).
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).

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

### VSCode integration

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

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

## Managing the SDK

Expand All @@ -57,8 +62,15 @@ To update the SDK, run `ufbt update`. This will download and install all require
- uFBT can also download and update the SDK from any **fixed URL**. To do this, run `ufbt update --url=<url>`.
- 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.

uFBT stores its state in `.ufbt` subfolder in your home directory. You can override this location by setting `UFBT_HOME` environment variable.
### Global and per-project SDK management

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.

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.

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.

You can also specify additional options when creating the `.env` file. See `ufbt dotenv_create --help` for more information.

### ufbt-bootstrap

Expand All @@ -69,3 +81,9 @@ Updating the SDK is handled by uFBT component called _bootstrap_. It has a dedic
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.

`ufbt-bootstrap` and SDK-related `ufbt` subcommands accept `--verbose` option that will print additional debug information.

## Contributing

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).

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).
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.4.3
0.2.5
81 changes: 77 additions & 4 deletions test.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import json
import subprocess
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory


# ufbt invokation & json status output
def ufbt_status() -> dict:
def ufbt_status(cwd=None) -> dict:
# Call "ufbt status --json" and return the parsed json
try:
status = subprocess.check_output(["ufbt", "status", "--json"])
status = subprocess.check_output(["ufbt", "status", "--json"], cwd=cwd)
except subprocess.CalledProcessError as e:
status = e.output
return json.loads(status)


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


# Test initial deployment
Expand Down Expand Up @@ -84,3 +86,74 @@ def test_target_mode_switches(self):
ufbt_exec(["update"])
status = ufbt_status()
self.assertEqual(previous_status, status)

def test_dotenv_basic(self):
ufbt_exec(["clean"])
status = ufbt_status()
self.assertEqual(status.get("error"), "SDK is not deployed")

ufbt_exec(["update", "-t", "f7"])
status = ufbt_status()
self.assertEqual(status.get("target"), "f7")
self.assertEqual(status.get("mode"), "channel")
self.assertEqual(status.get("details", {}).get("channel"), "release")

with TemporaryDirectory() as tmpdir:
local_dir = Path(tmpdir) / "local_env"
local_dir.mkdir(exist_ok=False)

ufbt_exec(["dotenv_create"], cwd=local_dir)
status = ufbt_status(cwd=local_dir)
self.assertEqual(status.get("target"), None)
self.assertIn(
str(local_dir.absolute()), str(Path(status.get("state_dir")).absolute())
)
self.assertEqual(status.get("error"), "SDK is not deployed")

ufbt_exec(["update", "-b", "dev"], cwd=local_dir)
status = ufbt_status(cwd=local_dir)
self.assertEqual(status.get("target"), "f7")
self.assertEqual(status.get("mode"), "branch")
self.assertEqual(status.get("details", {}).get("branch", ""), "dev")

status = ufbt_status()
self.assertEqual(status.get("target"), "f7")
self.assertEqual(status.get("mode"), "channel")

def test_dotenv_notoolchain(self):
with TemporaryDirectory() as tmpdir:
local_dir = Path(tmpdir) / "local_env"
local_dir.mkdir(exist_ok=False)

ufbt_exec(["dotenv_create"], cwd=local_dir)
status = ufbt_status(cwd=local_dir)

toolchain_path_local = status.get("toolchain_dir", "")
self.assertTrue(Path(toolchain_path_local).is_symlink())

# 2nd env
local_dir2 = Path(tmpdir) / "local_env2"
local_dir2.mkdir(exist_ok=False)

ufbt_exec(["dotenv_create", "--no-link-toolchain"], cwd=local_dir2)
status = ufbt_status(cwd=local_dir2)

toolchain_path_local2 = status.get("toolchain_dir", "")
self.assertFalse(Path(toolchain_path_local2).exists())

def test_path_with_spaces(self):
ufbt_exec(["clean"])
status = ufbt_status()
self.assertEqual(status.get("error"), "SDK is not deployed")

with TemporaryDirectory() as tmpdir:
local_dir = Path(tmpdir) / "path with spaces"
local_dir.mkdir(exist_ok=False)

ufbt_exec(["dotenv_create"], cwd=local_dir)
ufbt_exec(["update"], cwd=local_dir)
status = ufbt_status(cwd=local_dir)
self.assertNotIn("error", status)

ufbt_exec(["create", "APPID=myapp"], cwd=local_dir)
ufbt_exec(["faps"], cwd=local_dir)
34 changes: 33 additions & 1 deletion ufbt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,50 @@
import sys

from .bootstrap import (
DEFAULT_UFBT_HOME,
ENV_FILE_NAME,
bootstrap_cli,
bootstrap_subcommands,
get_ufbt_package_version,
DEFAULT_UFBT_HOME,
)

__version__ = get_ufbt_package_version()


def _load_env_file(env_file):
"""
Minimalistic implementation of env file parser.
Only supports lines in format `KEY=VALUE`.
Ignores comments (lines starting with #) and empty lines.
"""
if not os.path.exists(env_file):
return {}
env_vars = {}
with open(env_file) as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
key, value = line.split("=", 1)
env_vars[key] = value
return env_vars


def ufbt_cli():
# load environment variables from .env file in current directory
try:
env_vars = _load_env_file(ENV_FILE_NAME)
if env_vars:
os.environ.update(env_vars)
except Exception as e:
print(f"Failed to load environment variables from {ENV_FILE_NAME}: {e}")
return 2

if not os.environ.get("UFBT_HOME"):
os.environ["UFBT_HOME"] = DEFAULT_UFBT_HOME

os.environ["UFBT_HOME"] = os.path.abspath(os.environ["UFBT_HOME"])

# ufbt impl uses UFBT_STATE_DIR internally, not UFBT_HOME
os.environ["UFBT_STATE_DIR"] = os.environ["UFBT_HOME"]
if not os.environ.get("FBT_TOOLCHAIN_PATH"):
Expand Down
117 changes: 110 additions & 7 deletions ufbt/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import json
import logging
import os
import platform
import re
import shutil
import sys
Expand All @@ -38,6 +39,8 @@

log = logging.getLogger(__name__)
DEFAULT_UFBT_HOME = os.path.expanduser("~/.ufbt")
ENV_FILE_NAME = ".env"
STATE_DIR_TOOLCHAIN_SUBDIR = "toolchain"


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

def __init__(self, ufbt_state_dir: str):
def __init__(self, ufbt_state_dir: str, toolchain_dir: str = None):
self.ufbt_state_dir = Path(ufbt_state_dir)
self.download_dir = self.ufbt_state_dir / "download"
self.current_sdk_dir = self.ufbt_state_dir / "current"
self.toolchain_dir = (
Path(os.environ.get("FBT_TOOLCHAIN_PATH", self.ufbt_state_dir.absolute()))
/ "toolchain"
)
if toolchain_dir:
self.toolchain_dir = self.ufbt_state_dir / toolchain_dir
else:
self.toolchain_dir = (
Path(
os.environ.get("FBT_TOOLCHAIN_PATH", self.ufbt_state_dir.absolute())
)
/ STATE_DIR_TOOLCHAIN_SUBDIR
)
self.state_file = self.current_sdk_dir / self.UFBT_STATE_FILE_NAME

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

def _add_arguments(self, parser: argparse.ArgumentParser) -> None:
parser.description = """Update uFBT SDK. By default uses the last used target and mode.
Otherwise deploys latest release."""

parser.add_argument(
"--hw-target",
"-t",
Expand Down Expand Up @@ -611,6 +622,8 @@ def __init__(self):
super().__init__(self.COMMAND, "Clean uFBT SDK state")

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

def _add_arguments(self, parser: argparse.ArgumentParser) -> None:
parser.description = """Show uFBT status - deployment paths and SDK version."""

parser.add_argument(
"--json",
help="Print status in JSON format",
Expand Down Expand Up @@ -702,6 +717,7 @@ def _func(self, args) -> int:
else:
state_data.update({"error": "SDK is not deployed"})

skip_error_message = False
if key := args.status_key:
if key not in state_data:
log.error(f"Unknown status key {key}")
Expand All @@ -714,13 +730,100 @@ def _func(self, args) -> int:
if args.json:
print(json.dumps(state_data))
else:
skip_error_message = True
for key, value in state_data.items():
log.info(f"{self.STATUS_FIELDS[key]:<15} {value}")

return 1 if state_data.get("error") else 0
if state_data.get("error"):
if not skip_error_message:
log.error("Status error: {}".format(state_data.get("error")))
return 1
return 0


class LocalEnvSubcommand(CliSubcommand):
COMMAND = "dotenv_create"

def __init__(self):
super().__init__(self.COMMAND, "Create a local environment for uFBT")

def _add_arguments(self, parser: argparse.ArgumentParser) -> None:
parser.description = f"""Create a dotenv ({ENV_FILE_NAME}) file in current directory with environment variables for uFBT.
Designed for per-project SDK management.
If {ENV_FILE_NAME} file already exists, this command will refuse to overwrite it.
"""
parser.add_argument(
"--state-dir",
help="Directory to create the local environment in. Defaults to '.ufbt'.",
default=".ufbt",
)

parser.add_argument(
"--no-link-toolchain",
help="Don't link toolchain directory to the local environment and create a local copy",
action="store_true",
default=False,
)

bootstrap_subcommand_classes = (UpdateSubcommand, CleanSubcommand, StatusSubcommand)
@staticmethod
def _link_dir(target_path, source_path):
log.info(f"Linking {target_path=} to {source_path=}")
if os.path.lexists(target_path) or os.path.exists(target_path):
os.unlink(target_path)
if platform.system() == "Windows":
# Crete junction - does not require admin rights
import _winapi

if not os.path.isdir(source_path):
raise ValueError(f"Source path {source_path} is not a directory")

if not os.path.exists(target_path):
_winapi.CreateJunction(source_path, target_path)
else:
os.symlink(source_path, target_path)

def _func(self, args) -> int:
if os.path.exists(ENV_FILE_NAME):
log.error(
f"File {ENV_FILE_NAME} already exists, refusing to overwrite. Please remove or update it manually."
)
return 1

env_sdk_deployer = UfbtSdkDeployer(args.state_dir, STATE_DIR_TOOLCHAIN_SUBDIR)
# Will extract toolchain dir from env
default_sdk_deployer = UfbtSdkDeployer(args.ufbt_home)

env_sdk_deployer.ufbt_state_dir.mkdir(parents=True, exist_ok=True)
if args.no_link_toolchain:
log.info("Skipping toolchain directory linking")
else:
env_sdk_deployer.ufbt_state_dir.mkdir(parents=True, exist_ok=True)
default_sdk_deployer.toolchain_dir.mkdir(parents=True, exist_ok=True)
self._link_dir(
str(env_sdk_deployer.toolchain_dir.absolute()),
str(default_sdk_deployer.toolchain_dir.absolute()),
)
log.info("To use a local copy, specify --no-link-toolchain")

env_vars = {
"UFBT_HOME": args.state_dir,
# "TOOLCHAIN_PATH": str(env_sdk_deployer.toolchain_dir.absolute()),
}

with open(ENV_FILE_NAME, "wt") as f:
for key, value in env_vars.items():
f.write(f"{key}={value}\n")

log.info(f"Created {ENV_FILE_NAME} file in {os.getcwd()}")
return 0


bootstrap_subcommand_classes = (
UpdateSubcommand,
CleanSubcommand,
StatusSubcommand,
LocalEnvSubcommand,
)

bootstrap_subcommands = (
subcommand_cls.COMMAND for subcommand_cls in bootstrap_subcommand_classes
Expand Down

0 comments on commit c898427

Please sign in to comment.