[FL-2832] fbt: more fixes & improvements (#1854)

* github: bundling debug folder with scripts; docs: fixes & updates; fbt: added FAP_EXAMPLES variable to enable building example apps. Disabled by default. fbt: added TERM to list of proxied environment variables
* fbt: better help output; disabled implicit_deps_unchanged; added color to import validator reports
* fbt: moved debug configuration to separate tool
* fbt: proper dependency tracker for SDK source file; renamed linker script for external apps
* fbt: fixed debug elf path
* fbt: packaging sdk archive
* scripts: fixed sconsdist.py
* fbt: reworked sdk packing; docs: updates
* docs: info on cli target; linter fixes
* fbt: moved main code to scripts folder
* scripts: packing update into .tgz
* fbt, scripts: reworked copro_dist to build .tgz
* scripts: fixed naming for archived updater package
* Scripts: fix ぐるぐる回る

Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com>
This commit is contained in:
hedger
2022-10-12 20:12:05 +04:00
committed by GitHub
parent afff1adf8f
commit eb4ff3c0fd
41 changed files with 413 additions and 272 deletions

View File

@@ -0,0 +1,74 @@
from SCons.Errors import StopError
class BlackmagicResolver:
BLACKMAGIC_HOSTNAME = "blackmagic.local"
def __init__(self, env):
self.env = env
# On Win:
# 'location': '1-5:x.0', 'name': 'COM4',
# 'location': '1-5:x.2', 'name': 'COM13',
# On Linux:
# 'location': '1-1.2:1.0', 'name': 'ttyACM0',
# 'location': '1-1.2:1.2', 'name': 'ttyACM1',
# On MacOS:
# 'location': '0-1.3', 'name': 'cu.usbmodemblackmagic1',
# 'location': '0-1.3', 'name': 'cu.usbmodemblackmagic3',
def _find_probe(self):
import serial.tools.list_ports as list_ports
ports = list(list_ports.grep("blackmagic"))
if len(ports) == 0:
# Blackmagic probe serial port not found, will be handled later
pass
elif len(ports) > 2:
raise StopError("More than one Blackmagic probe found")
else:
# If you're getting any issues with auto lookup, uncomment this
# print("\n".join([f"{p.device} {vars(p)}" for p in ports]))
return sorted(ports, key=lambda p: f"{p.location}_{p.name}")[0]
# Look up blackmagic probe hostname with dns
def _resolve_hostname(self):
import socket
try:
return socket.gethostbyname(self.BLACKMAGIC_HOSTNAME)
except socket.gaierror:
print("Failed to resolve Blackmagic hostname")
return None
def get_serial(self):
if not (probe := self._find_probe()):
return None
# print(f"Found Blackmagic probe on {probe.device}")
if self.env.subst("$PLATFORM") == "win32":
return f"\\\\.\\{probe.device}"
return probe.device
def get_networked(self):
if not (probe := self._resolve_hostname()):
return None
return f"tcp:{probe}:2345"
def __str__(self):
# print("distenv blackmagic", self.env.subst("$BLACKMAGIC"))
if (blackmagic := self.env.subst("$BLACKMAGIC")) != "auto":
return blackmagic
# print("Looking for Blackmagic...")
if probe := self.get_serial() or self.get_networked():
return probe
raise StopError("Please specify BLACKMAGIC=...")
def generate(env):
env.SetDefault(BLACKMAGIC_ADDR=BlackmagicResolver(env))
def exists(env):
return True

View File

@@ -0,0 +1,14 @@
def exists():
return True
def generate(env):
if ccache := env.WhereIs("ccache"):
env["CCACHE"] = "ccache"
env["CC_NOCACHE"] = env["CC"]
env["CC"] = "$CCACHE $CC_NOCACHE"
# Tricky place: linking is done with CXX
# Using ccache breaks it
env["LINK"] = env["CXX"]
env["CXX_NOCACHE"] = env["CXX"]
env["CXX"] = "$CCACHE $CXX_NOCACHE"

View File

@@ -0,0 +1,73 @@
from SCons.Errors import StopError
from SCons.Tool import asm
from SCons.Tool import gcc
from SCons.Tool import gxx
from SCons.Tool import ar
from SCons.Tool import gnulink
import strip
import gdb
import objdump
from SCons.Action import _subproc
import subprocess
def prefix_commands(env, command_prefix, cmd_list):
for command in cmd_list:
if command in env:
env[command] = command_prefix + env[command]
def _get_tool_version(env, tool):
verstr = "version unknown"
proc = _subproc(
env,
env.subst("${%s} --version" % tool),
stdout=subprocess.PIPE,
stderr="devnull",
stdin="devnull",
universal_newlines=True,
error="raise",
shell=True,
)
if proc:
verstr = proc.stdout.readline()
proc.communicate()
return verstr
def generate(env, **kw):
for orig_tool in (asm, gcc, gxx, ar, gnulink, strip, gdb, objdump):
orig_tool.generate(env)
env.SetDefault(
TOOLCHAIN_PREFIX=kw.get("toolchain_prefix"),
)
prefix_commands(
env,
env.subst("$TOOLCHAIN_PREFIX"),
[
"AR",
"AS",
"CC",
"CXX",
"OBJCOPY",
"RANLIB",
"STRIP",
"GDB",
"GDBPY",
"OBJDUMP",
],
)
# Call CC to check version
if whitelisted_versions := kw.get("versions", ()):
cc_version = _get_tool_version(env, "CC")
# print("CC version =", cc_version)
# print(list(filter(lambda v: v in cc_version, whitelisted_versions)))
if not any(filter(lambda v: v in cc_version, whitelisted_versions)):
raise StopError(
f"Toolchain version is not supported. Allowed: {whitelisted_versions}, toolchain: {cc_version} "
)
def exists(env):
return True

View File

@@ -0,0 +1,85 @@
from SCons.Builder import Builder
from SCons.Action import Action
from SCons.Warnings import warn, WarningOnByDefault
import SCons
import os.path
from fbt.appmanifest import (
FlipperAppType,
AppManager,
ApplicationsCGenerator,
FlipperManifestException,
)
# Adding objects for application management to env
# AppManager env["APPMGR"] - loads all manifests; manages list of known apps
# AppBuildset env["APPBUILD"] - contains subset of apps, filtered for current config
def LoadApplicationManifests(env):
appmgr = env["APPMGR"] = AppManager()
for app_dir, _ in env["APPDIRS"]:
app_dir_node = env.Dir("#").Dir(app_dir)
for entry in app_dir_node.glob("*", ondisk=True, source=True):
if isinstance(entry, SCons.Node.FS.Dir) and not str(entry).startswith("."):
try:
app_manifest_file_path = os.path.join(
entry.abspath, "application.fam"
)
appmgr.load_manifest(app_manifest_file_path, entry)
env.Append(PY_LINT_SOURCES=[app_manifest_file_path])
except FlipperManifestException as e:
warn(WarningOnByDefault, str(e))
def PrepareApplicationsBuild(env):
appbuild = env["APPBUILD"] = env["APPMGR"].filter_apps(env["APPS"])
env.Append(
SDK_HEADERS=appbuild.get_sdk_headers(),
)
env["APPBUILD_DUMP"] = env.Action(
DumpApplicationConfig,
"\tINFO\t",
)
def DumpApplicationConfig(target, source, env):
print(f"Loaded {len(env['APPMGR'].known_apps)} app definitions.")
print("Firmware modules configuration:")
for apptype in FlipperAppType:
app_sublist = env["APPBUILD"].get_apps_of_type(apptype)
if app_sublist:
print(
f"{apptype.value}:\n\t",
", ".join(app.appid for app in app_sublist),
)
def build_apps_c(target, source, env):
target_file_name = target[0].path
gen = ApplicationsCGenerator(env["APPBUILD"], env.subst("$LOADER_AUTOSTART"))
with open(target_file_name, "w") as file:
file.write(gen.generate())
def generate(env):
env.AddMethod(LoadApplicationManifests)
env.AddMethod(PrepareApplicationsBuild)
env.Append(
BUILDERS={
"ApplicationsC": Builder(
action=Action(
build_apps_c,
"${APPSCOMSTR}",
),
suffix=".c",
),
}
)
def exists(env):
return True

View File

@@ -0,0 +1,175 @@
import SCons
from SCons.Builder import Builder
from SCons.Action import Action
from SCons.Node.FS import File
import os
import subprocess
def icons_emitter(target, source, env):
target = [
target[0].File(env.subst("${ICON_FILE_NAME}.c")),
target[0].File(env.subst("${ICON_FILE_NAME}.h")),
]
source = env.GlobRecursive("*.*", env["ICON_SRC_DIR"])
return target, source
def proto_emitter(target, source, env):
target = []
for src in source:
basename = os.path.splitext(src.name)[0]
target.append(env.File(f"compiled/{basename}.pb.c"))
target.append(env.File(f"compiled/{basename}.pb.h"))
return target, source
def dolphin_emitter(target, source, env):
res_root_dir = source[0].Dir(env["DOLPHIN_RES_TYPE"])
source = [res_root_dir]
source.extend(
env.GlobRecursive("*.*", res_root_dir),
)
target_base_dir = target[0]
env.Replace(_DOLPHIN_OUT_DIR=target[0])
if env["DOLPHIN_RES_TYPE"] == "external":
target = []
target.extend(
map(
lambda node: target_base_dir.File(
res_root_dir.rel_path(node).replace(".png", ".bm")
),
filter(lambda node: isinstance(node, SCons.Node.FS.File), source),
)
)
else:
asset_basename = f"assets_dolphin_{env['DOLPHIN_RES_TYPE']}"
target = [
target_base_dir.File(asset_basename + ".c"),
target_base_dir.File(asset_basename + ".h"),
]
return target, source
def _invoke_git(args, source_dir):
cmd = ["git"]
cmd.extend(args)
return (
subprocess.check_output(cmd, cwd=source_dir, stderr=subprocess.STDOUT)
.strip()
.decode()
)
def proto_ver_generator(target, source, env):
target_file = target[0]
src_dir = source[0].dir.abspath
try:
git_fetch = _invoke_git(
["fetch", "--tags"],
source_dir=src_dir,
)
except (subprocess.CalledProcessError, EnvironmentError) as e:
# Not great, not terrible
print("Git: fetch failed")
try:
git_describe = _invoke_git(
["describe", "--tags", "--abbrev=0"],
source_dir=src_dir,
)
except (subprocess.CalledProcessError, EnvironmentError) as e:
print("Git: describe failed")
Exit("git error")
# print("describe=", git_describe)
git_major, git_minor = git_describe.split(".")
version_file_data = (
"#pragma once",
f"#define PROTOBUF_MAJOR_VERSION {git_major}",
f"#define PROTOBUF_MINOR_VERSION {git_minor}",
"",
)
with open(str(target_file), "wt") as file:
file.write("\n".join(version_file_data))
def CompileIcons(env, target_dir, source_dir, *, icon_bundle_name="assets_icons"):
# Gathering icons sources
icons_src = env.GlobRecursive("*.png", source_dir)
icons_src += env.GlobRecursive("frame_rate", source_dir)
icons = env.IconBuilder(
target_dir,
ICON_SRC_DIR=source_dir,
ICON_FILE_NAME=icon_bundle_name,
)
env.Depends(icons, icons_src)
return icons
def generate(env):
env.SetDefault(
ASSETS_COMPILER="${ROOT_DIR.abspath}/scripts/assets.py",
NANOPB_COMPILER="${ROOT_DIR.abspath}/lib/nanopb/generator/nanopb_generator.py",
)
env.AddMethod(CompileIcons)
if not env["VERBOSE"]:
env.SetDefault(
ICONSCOMSTR="\tICONS\t${TARGET}",
PROTOCOMSTR="\tPROTO\t${SOURCE}",
DOLPHINCOMSTR="\tDOLPHIN\t${DOLPHIN_RES_TYPE}",
RESMANIFESTCOMSTR="\tMANIFEST\t${TARGET}",
PBVERCOMSTR="\tPBVER\t${TARGET}",
)
env.Append(
BUILDERS={
"IconBuilder": Builder(
action=Action(
'${PYTHON3} "${ASSETS_COMPILER}" icons ${ICON_SRC_DIR} ${TARGET.dir} --filename ${ICON_FILE_NAME}',
"${ICONSCOMSTR}",
),
emitter=icons_emitter,
),
"ProtoBuilder": Builder(
action=Action(
'${PYTHON3} "${NANOPB_COMPILER}" -q -I${SOURCE.dir.posix} -D${TARGET.dir.posix} ${SOURCES.posix}',
"${PROTOCOMSTR}",
),
emitter=proto_emitter,
suffix=".pb.c",
src_suffix=".proto",
),
"DolphinSymBuilder": Builder(
action=Action(
'${PYTHON3} "${ASSETS_COMPILER}" dolphin -s dolphin_${DOLPHIN_RES_TYPE} "${SOURCE}" "${_DOLPHIN_OUT_DIR}"',
"${DOLPHINCOMSTR}",
),
emitter=dolphin_emitter,
),
"DolphinExtBuilder": Builder(
action=Action(
'${PYTHON3} "${ASSETS_COMPILER}" dolphin "${SOURCE}" "${_DOLPHIN_OUT_DIR}"',
"${DOLPHINCOMSTR}",
),
emitter=dolphin_emitter,
),
"ProtoVerBuilder": Builder(
action=Action(
proto_ver_generator,
"${PBVERCOMSTR}",
),
),
}
)
def exists(env):
return True

View File

@@ -0,0 +1,41 @@
def generate(env, **kw):
env.SetDefault(
OPENOCD_GDB_PIPE=[
"|openocd -c 'gdb_port pipe; log_output debug/openocd.log' ${[SINGLEQUOTEFUNC(OPENOCD_OPTS)]}"
],
GDBOPTS_BASE=[
"-ex",
"target extended-remote ${GDBREMOTE}",
"-ex",
"set confirm off",
"-ex",
"set pagination off",
],
GDBOPTS_BLACKMAGIC=[
"-ex",
"monitor swdp_scan",
"-ex",
"monitor debug_bmp enable",
"-ex",
"attach 1",
"-ex",
"set mem inaccessible-by-default off",
],
GDBPYOPTS=[
"-ex",
"source debug/FreeRTOS/FreeRTOS.py",
"-ex",
"source debug/flipperapps.py",
"-ex",
"source debug/PyCortexMDebug/PyCortexMDebug.py",
"-ex",
"svd_load ${SVD_FILE}",
"-ex",
"compare-sections",
],
JFLASHPROJECT="${ROOT_DIR.abspath}/debug/fw.jflash",
)
def exists(env):
return True

View File

@@ -0,0 +1,155 @@
from SCons.Builder import Builder
from SCons.Action import Action
from SCons.Script import Mkdir
from SCons.Defaults import Touch
def GetProjetDirName(env, project=None):
parts = [f"f{env['TARGET_HW']}"]
if project:
parts.append(project)
suffix = ""
if env["DEBUG"]:
suffix += "D"
if env["COMPACT"]:
suffix += "C"
if suffix:
parts.append(suffix)
return "-".join(parts)
def create_fw_build_targets(env, configuration_name):
flavor = GetProjetDirName(env, configuration_name)
build_dir = env.Dir("build").Dir(flavor).abspath
return env.SConscript(
"firmware.scons",
variant_dir=build_dir,
duplicate=0,
exports={
"ENV": env,
"fw_build_meta": {
"type": configuration_name,
"flavor": flavor,
"build_dir": build_dir,
},
},
)
def AddFwProject(env, base_env, fw_type, fw_env_key):
project_env = env[fw_env_key] = create_fw_build_targets(base_env, fw_type)
env.Append(
DIST_PROJECTS=[
project_env["FW_FLAVOR"],
],
DIST_DEPENDS=[
project_env["FW_ARTIFACTS"],
],
)
env.Replace(DIST_DIR=env.GetProjetDirName())
return project_env
def AddOpenOCDFlashTarget(env, targetenv, **kw):
openocd_target = env.OpenOCDFlash(
"#build/oocd-${BUILD_CFG}-flash.flag",
targetenv["FW_BIN"],
OPENOCD_COMMAND=[
"-c",
"program ${SOURCE.posix} reset exit ${BASE_ADDRESS}",
],
BUILD_CFG=targetenv.subst("$FIRMWARE_BUILD_CFG"),
BASE_ADDRESS=targetenv.subst("$IMAGE_BASE_ADDRESS"),
**kw,
)
env.Alias(targetenv.subst("${FIRMWARE_BUILD_CFG}_flash"), openocd_target)
if env["FORCE"]:
env.AlwaysBuild(openocd_target)
return openocd_target
def AddJFlashTarget(env, targetenv, **kw):
jflash_target = env.JFlash(
"#build/jflash-${BUILD_CFG}-flash.flag",
targetenv["FW_BIN"],
JFLASHADDR=targetenv.subst("$IMAGE_BASE_ADDRESS"),
BUILD_CFG=targetenv.subst("${FIRMWARE_BUILD_CFG}"),
**kw,
)
env.Alias(targetenv.subst("${FIRMWARE_BUILD_CFG}_jflash"), jflash_target)
if env["FORCE"]:
env.AlwaysBuild(jflash_target)
return jflash_target
def AddUsbFlashTarget(env, file_flag, extra_deps, **kw):
usb_update = env.UsbInstall(
file_flag,
(
env["DIST_DEPENDS"],
*extra_deps,
),
)
if env["FORCE"]:
env.AlwaysBuild(usb_update)
return usb_update
def DistCommand(env, name, source, **kw):
target = f"dist_{name}"
command = env.Command(
target,
source,
'@${PYTHON3} "${ROOT_DIR.abspath}/scripts/sconsdist.py" copy -p ${DIST_PROJECTS} -s "${DIST_SUFFIX}" ${DIST_EXTRA}',
**kw,
)
env.Pseudo(target)
env.Alias(name, command)
return command
def generate(env):
env.AddMethod(AddFwProject)
env.AddMethod(DistCommand)
env.AddMethod(AddOpenOCDFlashTarget)
env.AddMethod(GetProjetDirName)
env.AddMethod(AddJFlashTarget)
env.AddMethod(AddUsbFlashTarget)
env.SetDefault(
COPRO_MCU_FAMILY="STM32WB5x",
)
env.Append(
BUILDERS={
"UsbInstall": Builder(
action=[
Action(
'${PYTHON3} "${ROOT_DIR.abspath}/scripts/selfupdate.py" dist/${DIST_DIR}/f${TARGET_HW}-update-${DIST_SUFFIX}/update.fuf'
),
Touch("${TARGET}"),
]
),
"CoproBuilder": Builder(
action=Action(
[
'${PYTHON3} "${ROOT_DIR.abspath}/scripts/assets.py" '
"copro ${COPRO_CUBE_DIR} "
"${TARGET} ${COPRO_MCU_FAMILY} "
"--cube_ver=${COPRO_CUBE_VERSION} "
"--stack_type=${COPRO_STACK_TYPE} "
'--stack_file="${COPRO_STACK_BIN}" '
"--stack_addr=${COPRO_STACK_ADDR} ",
],
"\tCOPRO\t${TARGET}",
)
),
}
)
def exists(env):
return True

View File

@@ -0,0 +1,259 @@
from SCons.Builder import Builder
from SCons.Action import Action
from SCons.Errors import UserError
import SCons.Warnings
import os
import pathlib
from fbt.elfmanifest import assemble_manifest_data
from fbt.appmanifest import FlipperApplication, FlipperManifestException
from fbt.sdk import SdkCache
import itertools
def BuildAppElf(env, app):
ext_apps_work_dir = env.subst("$EXT_APPS_WORK_DIR")
app_work_dir = os.path.join(ext_apps_work_dir, app.appid)
env.VariantDir(app_work_dir, app._appdir, duplicate=False)
app_env = env.Clone(FAP_SRC_DIR=app._appdir, FAP_WORK_DIR=app_work_dir)
app_alias = f"fap_{app.appid}"
# Deprecation stub
legacy_app_taget_name = f"{app_env['FIRMWARE_BUILD_CFG']}_{app.appid}"
def legacy_app_build_stub(**kw):
raise UserError(
f"Target name '{legacy_app_taget_name}' is deprecated, use '{app_alias}' instead"
)
app_env.PhonyTarget(legacy_app_taget_name, Action(legacy_app_build_stub, None))
externally_built_files = []
if app.fap_extbuild:
for external_file_def in app.fap_extbuild:
externally_built_files.append(external_file_def.path)
app_env.Alias(app_alias, external_file_def.path)
app_env.AlwaysBuild(
app_env.Command(
external_file_def.path,
None,
Action(
external_file_def.command,
"" if app_env["VERBOSE"] else "\tEXTCMD\t${TARGET}",
),
)
)
if app.fap_icon_assets:
app_env.CompileIcons(
app_env.Dir(app_work_dir),
app._appdir.Dir(app.fap_icon_assets),
icon_bundle_name=f"{app.appid}_icons",
)
private_libs = []
for lib_def in app.fap_private_libs:
lib_src_root_path = os.path.join(app_work_dir, "lib", lib_def.name)
app_env.AppendUnique(
CPPPATH=list(
app_env.Dir(lib_src_root_path).Dir(incpath).srcnode()
for incpath in lib_def.fap_include_paths
),
)
lib_sources = list(
itertools.chain.from_iterable(
app_env.GlobRecursive(source_type, lib_src_root_path)
for source_type in lib_def.sources
)
)
if len(lib_sources) == 0:
raise UserError(f"No sources gathered for private library {lib_def}")
private_lib_env = app_env.Clone()
private_lib_env.AppendUnique(
CCFLAGS=[
*lib_def.cflags,
],
CPPDEFINES=lib_def.cdefines,
CPPPATH=list(
os.path.join(app._appdir.path, cinclude)
for cinclude in lib_def.cincludes
),
)
lib = private_lib_env.StaticLibrary(
os.path.join(app_work_dir, lib_def.name),
lib_sources,
)
private_libs.append(lib)
app_sources = list(
itertools.chain.from_iterable(
app_env.GlobRecursive(
source_type,
app_work_dir,
exclude="lib",
)
for source_type in app.sources
)
)
app_env.Append(
LIBS=[*app.fap_libs, *private_libs],
CPPPATH=env.Dir(app_work_dir),
)
app_elf_raw = app_env.Program(
os.path.join(ext_apps_work_dir, f"{app.appid}_d"),
app_sources,
APP_ENTRY=app.entry_point,
)
app_env.Clean(app_elf_raw, [*externally_built_files, app_env.Dir(app_work_dir)])
app_elf_dump = app_env.ObjDump(app_elf_raw)
app_env.Alias(f"{app_alias}_list", app_elf_dump)
app_elf_augmented = app_env.EmbedAppMetadata(
os.path.join(ext_apps_work_dir, app.appid),
app_elf_raw,
APP=app,
)
manifest_vals = {
k: v
for k, v in vars(app).items()
if not k.startswith(FlipperApplication.PRIVATE_FIELD_PREFIX)
}
app_env.Depends(
app_elf_augmented,
[app_env["SDK_DEFINITION"], app_env.Value(manifest_vals)],
)
if app.fap_icon:
app_env.Depends(
app_elf_augmented,
app_env.File(f"{app._apppath}/{app.fap_icon}"),
)
app_elf_import_validator = app_env.ValidateAppImports(app_elf_augmented)
app_env.AlwaysBuild(app_elf_import_validator)
app_env.Alias(app_alias, app_elf_import_validator)
return (app_elf_augmented, app_elf_raw, app_elf_import_validator)
def prepare_app_metadata(target, source, env):
sdk_cache = SdkCache(env.subst("$SDK_DEFINITION"), load_version_only=True)
if not sdk_cache.is_buildable():
raise UserError(
"SDK version is not finalized, please review changes and re-run operation"
)
app = env["APP"]
meta_file_name = source[0].path + ".meta"
with open(meta_file_name, "wb") as f:
# f.write(f"hello this is {app}")
f.write(
assemble_manifest_data(
app_manifest=app,
hardware_target=int(env.subst("$TARGET_HW")),
sdk_version=sdk_cache.version.as_int(),
)
)
def validate_app_imports(target, source, env):
sdk_cache = SdkCache(env.subst("$SDK_DEFINITION"), load_version_only=False)
app_syms = set()
with open(target[0].path, "rt") as f:
for line in f:
app_syms.add(line.split()[0])
unresolved_syms = app_syms - sdk_cache.get_valid_names()
if unresolved_syms:
SCons.Warnings.warn(
SCons.Warnings.LinkWarning,
f"\033[93m{source[0].path}: app won't run. Unresolved symbols: \033[95m{unresolved_syms}\033[0m",
)
def GetExtAppFromPath(env, app_dir):
if not app_dir:
raise UserError("APPSRC= not set")
appmgr = env["APPMGR"]
app = None
try:
# Maybe used passed an appid?
app = appmgr.get(app_dir)
except FlipperManifestException as _:
# Look up path components in known app dits
for dir_part in reversed(pathlib.Path(app_dir).parts):
if app := appmgr.find_by_appdir(dir_part):
break
if not app:
raise UserError(f"Failed to resolve application for given APPSRC={app_dir}")
app_elf = env["_extapps"]["compact"].get(app.appid, None)
if not app_elf:
raise UserError(
f"Application {app.appid} is not configured for building as external"
)
app_validator = env["_extapps"]["validators"].get(app.appid, None)
return (app, app_elf[0], app_validator[0])
def generate(env, **kw):
env.SetDefault(EXT_APPS_WORK_DIR=kw.get("EXT_APPS_WORK_DIR"))
# env.VariantDir(env.subst("$EXT_APPS_WORK_DIR"), env.Dir("#"), duplicate=False)
env.AddMethod(BuildAppElf)
env.AddMethod(GetExtAppFromPath)
env.Append(
BUILDERS={
"EmbedAppMetadata": Builder(
action=[
Action(prepare_app_metadata, "$APPMETA_COMSTR"),
Action(
"${OBJCOPY} "
"--remove-section .ARM.attributes "
"--add-section .fapmeta=${SOURCE}.meta "
"--set-section-flags .fapmeta=contents,noload,readonly,data "
"--strip-debug --strip-unneeded "
"--add-gnu-debuglink=${SOURCE} "
"${SOURCES} ${TARGET}",
"$APPMETAEMBED_COMSTR",
),
],
suffix=".fap",
src_suffix=".elf",
),
"ValidateAppImports": Builder(
action=[
Action(
"@${NM} -P -u ${SOURCE} > ${TARGET}",
None, # "$APPDUMP_COMSTR",
),
Action(
validate_app_imports,
"$APPCHECK_COMSTR",
),
],
suffix=".impsyms",
src_suffix=".fap",
),
}
)
def exists(env):
return True

View File

@@ -0,0 +1,44 @@
targets_help = """Configuration variables:
"""
tail_help = """
TASKS:
Building:
firmware_all, fw_dist:
Build firmware; create distribution package
faps, fap_dist:
Build all FAP apps
fap_{APPID}, launch_app APPSRC={APPID}:
Build FAP app with appid={APPID}; upload & start it over USB
Flashing & debugging:
flash, flash_blackmagic, jflash:
Flash firmware to target using debug probe
flash_usb, flash_usb_full:
Install firmware using self-update package
debug, debug_other, blackmagic:
Start GDB
Other:
cli:
Open a Flipper CLI session over USB
firmware_cdb, updater_cdb:
Generate сompilation_database.json
lint, lint_py:
run linters
format, format_py:
run code formatters
For more targets & info, see documentation/fbt.md
"""
def generate(env, **kw):
vars = kw["vars"]
basic_help = vars.GenerateHelpText(env)
env.Help(targets_help + basic_help + tail_help)
def exists(env):
return True

View File

@@ -0,0 +1,263 @@
from SCons.Builder import Builder
from SCons.Action import Action
from SCons.Errors import UserError
# from SCons.Scanner import C
from SCons.Script import Mkdir, Copy, Delete, Entry
from SCons.Util import LogicalLines
import os.path
import posixpath
import pathlib
import json
from fbt.sdk import SdkCollector, SdkCache
def ProcessSdkDepends(env, filename):
try:
with open(filename, "r") as fin:
lines = LogicalLines(fin).readlines()
except IOError:
return []
_, depends = lines[0].split(":", 1)
depends = depends.split()
depends.pop(0) # remove the .c file
depends = list(
# Don't create dependency on non-existing files
# (e.g. when they were renamed since last build)
filter(
lambda file: file.exists(),
(env.File(f"#{path}") for path in depends),
)
)
return depends
def prebuild_sdk_emitter(target, source, env):
target.append(env.ChangeFileExtension(target[0], ".d"))
target.append(env.ChangeFileExtension(target[0], ".i.c"))
return target, source
def prebuild_sdk_create_origin_file(target, source, env):
mega_file = env.subst("${TARGET}.c", target=target[0])
with open(mega_file, "wt") as sdk_c:
sdk_c.write("\n".join(f"#include <{h.path}>" for h in env["SDK_HEADERS"]))
class SdkMeta:
def __init__(self, env):
self.env = env
def save_to(self, json_manifest_path: str):
meta_contents = {
"sdk_symbols": self.env["SDK_DEFINITION"].name,
"cc_args": self._wrap_scons_vars("$CCFLAGS $_CCCOMCOM"),
"cpp_args": self._wrap_scons_vars("$CXXFLAGS $CCFLAGS $_CCCOMCOM"),
"linker_args": self._wrap_scons_vars("$LINKFLAGS"),
}
with open(json_manifest_path, "wt") as f:
json.dump(meta_contents, f, indent=4)
def _wrap_scons_vars(self, vars: str):
expanded_vars = self.env.subst(vars, target=Entry("dummy"))
return expanded_vars.replace("\\", "/")
class SdkTreeBuilder:
def __init__(self, env, target, source) -> None:
self.env = env
self.target = target
self.source = source
self.header_depends = []
self.header_dirs = []
self.target_sdk_dir_name = env.subst("f${TARGET_HW}_sdk")
self.sdk_root_dir = target[0].Dir(".")
self.sdk_deploy_dir = self.sdk_root_dir.Dir(self.target_sdk_dir_name)
def _parse_sdk_depends(self):
deps_file = self.source[0]
with open(deps_file.path, "rt") as deps_f:
lines = LogicalLines(deps_f).readlines()
_, depends = lines[0].split(":", 1)
self.header_depends = list(
filter(lambda fname: fname.endswith(".h"), depends.split()),
)
self.header_dirs = sorted(
set(map(os.path.normpath, map(os.path.dirname, self.header_depends)))
)
def _generate_sdk_meta(self):
filtered_paths = [self.target_sdk_dir_name]
full_fw_paths = list(
map(
os.path.normpath,
(self.env.Dir(inc_dir).relpath for inc_dir in self.env["CPPPATH"]),
)
)
sdk_dirs = ", ".join(f"'{dir}'" for dir in self.header_dirs)
for dir in full_fw_paths:
if dir in sdk_dirs:
filtered_paths.append(
posixpath.normpath(posixpath.join(self.target_sdk_dir_name, dir))
)
sdk_env = self.env.Clone()
sdk_env.Replace(CPPPATH=filtered_paths)
meta = SdkMeta(sdk_env)
meta.save_to(self.target[0].path)
def emitter(self, target, source, env):
target_folder = target[0]
target = [target_folder.File("sdk.opts")]
return target, source
def _create_deploy_commands(self):
dirs_to_create = set(
self.sdk_deploy_dir.Dir(dirpath) for dirpath in self.header_dirs
)
actions = [
Delete(self.sdk_deploy_dir),
Mkdir(self.sdk_deploy_dir),
Copy(
self.sdk_root_dir,
self.env["SDK_DEFINITION"],
),
]
actions += [Mkdir(d) for d in dirs_to_create]
actions += [
Action(
Copy(self.sdk_deploy_dir.File(h).path, h),
# f"Copy {h} to {self.sdk_deploy_dir}",
)
for h in self.header_depends
]
return actions
def generate_actions(self):
self._parse_sdk_depends()
self._generate_sdk_meta()
return self._create_deploy_commands()
def deploy_sdk_tree(target, source, env, for_signature):
if for_signature:
return []
sdk_tree = SdkTreeBuilder(env, target, source)
return sdk_tree.generate_actions()
def deploy_sdk_tree_emitter(target, source, env):
sdk_tree = SdkTreeBuilder(env, target, source)
return sdk_tree.emitter(target, source, env)
def gen_sdk_data(sdk_cache: SdkCache):
api_def = []
api_def.extend(
(f"#include <{h.name}>" for h in sdk_cache.get_headers()),
)
api_def.append(f"const int elf_api_version = {sdk_cache.version.as_int()};")
api_def.append(
"static constexpr auto elf_api_table = sort(create_array_t<sym_entry>("
)
api_lines = []
for fun_def in sdk_cache.get_functions():
api_lines.append(
f"API_METHOD({fun_def.name}, {fun_def.returns}, ({fun_def.params}))"
)
for var_def in sdk_cache.get_variables():
api_lines.append(f"API_VARIABLE({var_def.name}, {var_def.var_type })")
api_def.append(",\n".join(api_lines))
api_def.append("));")
return api_def
def _check_sdk_is_up2date(sdk_cache: SdkCache):
if not sdk_cache.is_buildable():
raise UserError(
"SDK version is not finalized, please review changes and re-run operation"
)
def validate_sdk_cache(source, target, env):
# print(f"Generating SDK for {source[0]} to {target[0]}")
current_sdk = SdkCollector()
current_sdk.process_source_file_for_sdk(source[0].path)
for h in env["SDK_HEADERS"]:
current_sdk.add_header_to_sdk(pathlib.Path(h.path).as_posix())
sdk_cache = SdkCache(target[0].path)
sdk_cache.validate_api(current_sdk.get_api())
sdk_cache.save()
_check_sdk_is_up2date(sdk_cache)
def generate_sdk_symbols(source, target, env):
sdk_cache = SdkCache(source[0].path)
_check_sdk_is_up2date(sdk_cache)
api_def = gen_sdk_data(sdk_cache)
with open(target[0].path, "wt") as f:
f.write("\n".join(api_def))
def generate(env, **kw):
env.AddMethod(ProcessSdkDepends)
env.Append(
BUILDERS={
"SDKPrebuilder": Builder(
emitter=prebuild_sdk_emitter,
action=[
Action(
prebuild_sdk_create_origin_file,
"$SDK_PREGEN_COMSTR",
),
Action(
"$CC -o $TARGET -E -P $CCFLAGS $_CCCOMCOM $SDK_PP_FLAGS -MMD ${TARGET}.c",
"$SDK_COMSTR",
),
],
suffix=".i",
),
"SDKTree": Builder(
generator=deploy_sdk_tree,
emitter=deploy_sdk_tree_emitter,
src_suffix=".d",
),
"SDKSymUpdater": Builder(
action=Action(
validate_sdk_cache,
"$SDKSYM_UPDATER_COMSTR",
),
suffix=".csv",
src_suffix=".i",
),
"SDKSymGenerator": Builder(
action=Action(
generate_sdk_symbols,
"$SDKSYM_GENERATOR_COMSTR",
),
suffix=".h",
src_suffix=".csv",
),
}
)
def exists(env):
return True

View File

@@ -0,0 +1,29 @@
from SCons.Builder import Builder
from SCons.Action import Action
def version_emitter(target, source, env):
target_dir = target[0]
target = [
target_dir.File("version.inc.h"),
target_dir.File("version.json"),
]
return target, source
def generate(env):
env.Append(
BUILDERS={
"VersionBuilder": Builder(
action=Action(
'${PYTHON3} "${ROOT_DIR.abspath}/scripts/version.py" generate -t ${TARGET_HW} -o ${TARGET.dir.posix} --dir "${ROOT_DIR}"',
"${VERSIONCOMSTR}",
),
emitter=version_emitter,
),
}
)
def exists(env):
return True

View File

@@ -0,0 +1,54 @@
from SCons.Builder import Builder
from SCons.Action import Action
import SCons
__OBJCOPY_ARM_BIN = "arm-none-eabi-objcopy"
__NM_ARM_BIN = "arm-none-eabi-nm"
def generate(env):
env.SetDefault(
BIN2DFU="${ROOT_DIR.abspath}/scripts/bin2dfu.py",
OBJCOPY=__OBJCOPY_ARM_BIN, # FIXME
NM=__NM_ARM_BIN, # FIXME
)
env.Append(
BUILDERS={
"HEXBuilder": Builder(
action=Action(
'${OBJCOPY} -O ihex "${SOURCE}" "${TARGET}"',
"${HEXCOMSTR}",
),
suffix=".hex",
src_suffix=".elf",
),
"BINBuilder": Builder(
action=Action(
'${OBJCOPY} -O binary -S "${SOURCE}" "${TARGET}"',
"${BINCOMSTR}",
),
suffix=".bin",
src_suffix=".elf",
),
"DFUBuilder": Builder(
action=Action(
'${PYTHON3} "${BIN2DFU}" -i "${SOURCE}" -o "${TARGET}" -a ${IMAGE_BASE_ADDRESS} -l "Flipper Zero F${TARGET_HW}"',
"${DFUCOMSTR}",
),
suffix=".dfu",
src_suffix=".bin",
),
}
)
def exists(env):
try:
return env["OBJCOPY"]
except KeyError:
pass
if objcopy := env.WhereIs(__OBJCOPY_ARM_BIN):
return objcopy
raise SCons.Errors.StopError("Could not detect objcopy for arm")

17
scripts/fbt_tools/gdb.py Normal file
View File

@@ -0,0 +1,17 @@
from SCons.Builder import Builder
from SCons.Action import Action
def generate(env):
env.SetDefault(
GDB="gdb",
GDBPY="gdb-py",
GDBOPTS="",
GDBPYOPTS="",
GDBCOM="$GDB $GDBOPTS $SOURCES", # no $TARGET
GDBPYCOM="$GDBPY $GDBOPTS $GDBPYOPTS $SOURCES", # no $TARGET
)
def exists(env):
return True

View File

@@ -0,0 +1,27 @@
from SCons.Builder import Builder
from SCons.Defaults import Touch
def generate(env):
env.SetDefault(
JFLASH="JFlash" if env.subst("$PLATFORM") == "win32" else "JFlashExe",
JFLASHFLAGS=[
"-auto",
"-exit",
],
JFLASHCOM="${JFLASH} -openprj${JFLASHPROJECT} -open${SOURCE},${JFLASHADDR} ${JFLASHFLAGS}",
)
env.Append(
BUILDERS={
"JFlash": Builder(
action=[
"${JFLASHCOM}",
Touch("${TARGET}"),
],
),
}
)
def exists(env):
return True

View File

@@ -0,0 +1,26 @@
from SCons.Builder import Builder
from SCons.Action import Action
def generate(env):
env.SetDefault(
OBJDUMP="objdump",
OBJDUMPFLAGS=[],
OBJDUMPCOM="$OBJDUMP $OBJDUMPFLAGS -S $SOURCES > $TARGET",
)
env.Append(
BUILDERS={
"ObjDump": Builder(
action=Action(
"${OBJDUMPCOM}",
"${OBJDUMPCOMSTR}",
),
suffix=".lst",
src_suffix=".elf",
),
}
)
def exists(env):
return True

View File

@@ -0,0 +1,46 @@
from SCons.Builder import Builder
from SCons.Action import Action
from SCons.Defaults import Touch
import SCons
__OPENOCD_BIN = "openocd"
_oocd_action = Action(
"${OPENOCD} ${OPENOCD_OPTS} ${OPENOCD_COMMAND}",
"${OPENOCDCOMSTR}",
)
def generate(env):
env.SetDefault(
OPENOCD=__OPENOCD_BIN,
OPENOCD_OPTS="",
OPENOCD_COMMAND="",
OPENOCDCOM="${OPENOCD} ${OPENOCD_OPTS} ${OPENOCD_COMMAND}",
OPENOCDCOMSTR="",
)
env.Append(
BUILDERS={
"OpenOCDFlash": Builder(
action=[
_oocd_action,
Touch("${TARGET}"),
],
suffix=".flash",
src_suffix=".bin",
),
}
)
def exists(env):
try:
return env["OPENOCD"]
except KeyError:
pass
if openocd := env.WhereIs(__OPENOCD_BIN):
return openocd
raise SCons.Errors.StopError("Could not detect openocd")

View File

@@ -0,0 +1,13 @@
def generate(env):
py_name = "python3"
if env["PLATFORM"] == "win32":
# On Windows, Python 3 executable is usually just "python"
py_name = "python"
env.SetDefault(
PYTHON3=py_name,
)
def exists(env):
return True

View File

@@ -0,0 +1,55 @@
import posixpath
import os
from SCons.Errors import UserError
def BuildModule(env, module):
src_dir = str(env.Dir(".").srcdir or os.getcwd())
module_sconscript = posixpath.join(src_dir, module, "SConscript")
if not os.path.exists(module_sconscript):
module_sconscript = posixpath.join(src_dir, f"{module}.scons")
if not os.path.exists(module_sconscript):
raise UserError(f"Cannot build module {module}: scons file not found")
env.Append(PY_LINT_SOURCES=[module_sconscript])
return env.SConscript(
module_sconscript,
variant_dir=posixpath.join(env.subst("$BUILD_DIR"), module),
duplicate=0,
)
def BuildModules(env, modules):
result = []
for module in modules:
build_res = env.BuildModule(module)
# print("module ", module, build_res)
if build_res is None:
continue
result.append(build_res)
return result
def PhonyTarget(env, name, action, source=None, **kw):
if not source:
source = []
phony_name = "phony_" + name
env.Pseudo(phony_name)
command = env.Command(phony_name, source, action, **kw)
env.AlwaysBuild(env.Alias(name, command))
return command
def ChangeFileExtension(env, fnode, ext):
return env.File(f"#{os.path.splitext(fnode.path)[0]}{ext}")
def generate(env):
env.AddMethod(BuildModule)
env.AddMethod(BuildModules)
env.AddMethod(PhonyTarget)
env.AddMethod(ChangeFileExtension)
def exists(env):
return True

View File

@@ -0,0 +1,25 @@
import SCons
def GlobRecursive(env, pattern, node=".", exclude=None):
results = []
if isinstance(node, str):
node = env.Dir(node)
for f in node.glob("*", source=True, exclude=exclude):
if isinstance(f, SCons.Node.FS.Dir):
results += env.GlobRecursive(pattern, f, exclude)
results += node.glob(
pattern,
source=True,
exclude=exclude,
)
# print(f"Glob for {pattern} from {node}: {results}")
return results
def generate(env):
env.AddMethod(GlobRecursive)
def exists(env):
return True

View File

@@ -0,0 +1,26 @@
from SCons.Builder import Builder
from SCons.Action import Action
def generate(env):
env.SetDefault(
STRIP="strip",
STRIPFLAGS=[],
STRIPCOM="$STRIP $STRIPFLAGS $SOURCES -o $TARGET",
)
env.Append(
BUILDERS={
"ELFStripper": Builder(
action=Action(
"${STRIPCOM}",
"${STRIPCOMSTR}",
),
suffix=".elf",
src_suffix=".elf",
),
}
)
def exists(env):
return True