[FL-2052] New build system based on scons (#1269)

This commit is contained in:
hedger
2022-06-26 15:00:03 +03:00
committed by GitHub
parent c79fb61909
commit f3b1475ede
179 changed files with 3986 additions and 5196 deletions

47
site_scons/cc.scons Normal file
View File

@@ -0,0 +1,47 @@
Import("ENV")
ENV.AppendUnique(
CFLAGS=[
"-std=gnu17",
],
CXXFLAGS=[
"-std=c++17",
"-fno-rtti",
"-fno-use-cxa-atexit",
"-fno-exceptions",
"-fno-threadsafe-statics",
],
CCFLAGS=[
"-mcpu=cortex-m4",
"-mfloat-abi=hard",
"-mfpu=fpv4-sp-d16",
"-mthumb",
# "-MMD",
# "-MP",
"-Wall",
"-Wextra",
"-Werror",
"-Wno-address-of-packed-member",
"-Wredundant-decls",
"-Wdouble-promotion",
"-fdata-sections",
"-ffunction-sections",
"-fsingle-precision-constant",
"-fno-math-errno",
"-fstack-usage",
"-g",
# "-Wno-stringop-overread",
# "-Wno-stringop-overflow",
],
CPPDEFINES=[
"_GNU_SOURCE",
],
LINKFLAGS=[
"-mcpu=cortex-m4",
"-mfloat-abi=hard",
"-mfpu=fpv4-sp-d16",
"-mlittle-endian",
"-mthumb",
],
)

View File

@@ -0,0 +1,202 @@
# Commandline options
# To build updater-related targets, you need to set this option
AddOption(
"--with-updater",
dest="fullenv",
action="store_true",
help="Full firmware environment",
)
AddOption(
"--options",
dest="optionfile",
type="string",
nargs=1,
action="store",
default="fbt_options.py",
help="Enviroment option file",
)
AddOption(
"--extra-int-apps",
action="store",
dest="extra_int_apps",
default="",
help="List of applications to add to firmare's built-ins. Also see FIRMWARE_APP_SET and FIRMWARE_APPS",
)
AddOption(
"--extra-ext-apps",
action="store",
dest="extra_ext_apps",
default="",
help="List of applications to forcefully build as standalone .elf",
)
# Construction environment variables
vars = Variables(GetOption("optionfile"), ARGUMENTS)
vars.AddVariables(
BoolVariable(
"VERBOSE",
help="Print full commands",
default=False,
),
BoolVariable(
"FORCE",
help="Force target action (for supported targets)",
default=False,
),
BoolVariable(
"DEBUG",
help="Enable debug build",
default=True,
),
BoolVariable(
"COMPACT",
help="Optimize for size",
default=False,
),
EnumVariable(
"TARGET_HW",
help="Hardware target",
default="7",
allowed_values=[
"7",
],
),
)
vars.Add(
"DIST_SUFFIX",
help="Suffix for binaries in build output for dist targets",
default="local",
)
vars.Add(
"UPDATE_VERSION_STRING",
help="Version string for updater package",
default="${DIST_SUFFIX}",
)
vars.Add(
"COPRO_CUBE_VERSION",
help="Cube version",
default="",
)
vars.Add(
"COPRO_STACK_ADDR",
help="Core2 Firmware address",
default="0",
)
vars.Add(
"COPRO_STACK_BIN",
help="Core2 Firmware file name",
default="",
)
vars.Add(
"COPRO_DISCLAIMER",
help="Value to pass to bundling script to confirm dangerous operations",
default="",
)
vars.AddVariables(
PathVariable(
"COPRO_OB_DATA",
help="Path to OB reference data",
validator=PathVariable.PathIsFile,
default="",
),
PathVariable(
"COPRO_STACK_BIN_DIR",
help="Path to ST-provided stacks",
validator=PathVariable.PathIsDir,
default="",
),
PathVariable(
"COPRO_CUBE_DIR",
help="Path to Cube root",
validator=PathVariable.PathIsDir,
default="",
),
EnumVariable(
"COPRO_STACK_TYPE",
help="Core2 stack type",
default="ble_light",
allowed_values=[
"ble_full",
"ble_light",
"ble_basic",
],
),
PathVariable(
"SVD_FILE",
help="Path to SVD file",
validator=PathVariable.PathIsFile,
default="",
),
PathVariable(
"OTHER_ELF",
help="Path to prebuilt ELF file to debug",
validator=PathVariable.PathAccept,
default="",
),
)
vars.Add(
"FBT_TOOLCHAIN_VERSIONS",
help="Whitelisted toolchain versions (leave empty for no check)",
default=tuple(),
)
vars.Add(
"OPENOCD_OPTS",
help="Options to pass to OpenOCD",
default="",
)
vars.Add(
"UPDATE_SPLASH",
help="Directory name with slideshow frames to render after installing update package",
default="update_default",
)
vars.Add(
"FIRMWARE_APPS",
help="Map of (configuration_name->application_list)",
default={
"default": (
"crypto_start",
# Svc
"basic_services",
# Apps
"basic_apps",
"updater_app",
"archive",
# Settings
"passport",
"system_settings",
"about",
# Plugins
"basic_plugins",
# Debug
"debug_apps",
)
},
)
vars.Add(
"FIRMWARE_APP_SET",
help="Application set to use from FIRMWARE_APPS",
default="default",
)
Return("vars")

82
site_scons/environ.scons Normal file
View File

@@ -0,0 +1,82 @@
import SCons
from SCons.Platform import TempFileMunge
from fbt import util
import os
import multiprocessing
Import("VAR_ENV")
forward_os_env = {
# Import PATH from OS env - scons doesn't do that by default
"PATH": os.environ["PATH"],
}
# Proxying CI environment to child processes & scripts
for env_value_name in ("WORKFLOW_BRANCH_OR_TAG", "DIST_SUFFIX", "HOME", "APPDATA"):
if environ_value := os.environ.get(env_value_name, None):
forward_os_env[env_value_name] = environ_value
coreenv = VAR_ENV.Clone(
tools=[
(
"crosscc",
{
"toolchain_prefix": "arm-none-eabi-",
"versions": VAR_ENV["FBT_TOOLCHAIN_VERSIONS"],
},
),
"python3",
"sconsmodular",
"sconsrecursiveglob",
"ccache",
],
TEMPFILE=TempFileMunge,
MAXLINELENGTH=2048,
PROGSUFFIX=".elf",
ENV=forward_os_env,
)
# If DIST_SUFFIX is set in environment, is has precedence (set by CI)
if os_suffix := os.environ.get("DIST_SUFFIX", None):
coreenv.Replace(
DIST_SUFFIX=os_suffix,
)
# print(coreenv.Dump())
if not coreenv["VERBOSE"]:
coreenv.SetDefault(
CCCOMSTR="\tCC\t${SOURCE}",
CXXCOMSTR="\tCPP\t${SOURCE}",
ASCOMSTR="\tASM\t${SOURCE}",
ARCOMSTR="\tAR\t${TARGET}",
RANLIBCOMSTR="\tRANLIB\t${TARGET}",
LINKCOMSTR="\tLINK\t${TARGET}",
INSTALLSTR="\tINSTALL\t${TARGET}",
APPSCOMSTR="\tAPPS\t${TARGET}",
VERSIONCOMSTR="\tVERSION\t${TARGET}",
STRIPCOMSTR="\tSTRIP\t${TARGET}",
OBJDUMPCOMSTR="\tOBJDUMP\t${TARGET}",
# GDBCOMSTR="\tGDB\t${SOURCE}",
# GDBPYCOMSTR="\tGDB-PY\t${SOURCE}",
)
# Default value for commandline options
SetOption("num_jobs", multiprocessing.cpu_count())
# Avoiding re-scan of all sources on every startup
SetOption("implicit_cache", True)
SetOption("implicit_deps_unchanged", True)
# More aggressive caching
SetOption("max_drift", 1)
# Random task queue - to discover isses with build logic faster
# SetOption("random", 1)
# Setting up temp file parameters - to overcome command line length limits
coreenv["TEMPFILEARGESCFUNC"] = util.tempfile_arg_esc_func
util.wrap_tempfile(coreenv, "LINKCOM")
util.wrap_tempfile(coreenv, "ARCOM")
Return("coreenv")

View File

View File

@@ -0,0 +1,243 @@
from dataclasses import dataclass, field
from typing import List, Optional
from enum import Enum
import os
class FlipperManifestException(Exception):
pass
class FlipperAppType(Enum):
SERVICE = "Service"
SYSTEM = "System"
APP = "App"
PLUGIN = "Plugin"
DEBUG = "Debug"
ARCHIVE = "Archive"
SETTINGS = "Settings"
STARTUP = "StartupHook"
EXTERNAL = "External"
METAPACKAGE = "Package"
@dataclass
class FlipperApplication:
appid: str
apptype: FlipperAppType
name: Optional[str] = None
entry_point: Optional[str] = None
flags: List[str] = field(default_factory=lambda: ["Default"])
cdefines: List[str] = field(default_factory=list)
requires: List[str] = field(default_factory=list)
conflicts: List[str] = field(default_factory=list)
provides: List[str] = field(default_factory=list)
stack_size: int = 2048
icon: Optional[str] = None
order: int = 0
_appdir: Optional[str] = None
class AppManager:
def __init__(self):
self.known_apps = {}
def get(self, appname: str):
try:
return self.known_apps[appname]
except KeyError as _:
raise FlipperManifestException(
f"Missing application manifest for '{appname}'"
)
def load_manifest(self, app_manifest_path: str, app_dir_name: str):
if not os.path.exists(app_manifest_path):
raise FlipperManifestException(
f"App manifest not found at path {app_manifest_path}"
)
# print("Loading", app_manifest_path)
app_manifests = []
def App(*args, **kw):
nonlocal app_manifests
app_manifests.append(FlipperApplication(*args, **kw, _appdir=app_dir_name))
with open(app_manifest_path, "rt") as manifest_file:
exec(manifest_file.read())
if len(app_manifests) == 0:
raise FlipperManifestException(
f"App manifest '{app_manifest_path}' is malformed"
)
# print("Built", app_manifests)
for app in app_manifests:
self._add_known_app(app)
def _add_known_app(self, app: FlipperApplication):
if self.known_apps.get(app.appid, None):
raise FlipperManifestException(f"Duplicate app declaration: {app.appid}")
self.known_apps[app.appid] = app
def filter_apps(self, applist: List[str]):
return AppBuildset(self, applist)
class AppBuilderException(Exception):
pass
class AppBuildset:
BUILTIN_APP_TYPES = (
FlipperAppType.SERVICE,
FlipperAppType.SYSTEM,
FlipperAppType.APP,
FlipperAppType.PLUGIN,
FlipperAppType.DEBUG,
FlipperAppType.ARCHIVE,
FlipperAppType.SETTINGS,
FlipperAppType.STARTUP,
)
def __init__(self, appmgr: AppManager, appnames: List[str]):
self.appmgr = appmgr
self.appnames = set(appnames)
self._orig_appnames = appnames
self._process_deps()
self._check_conflicts()
self._check_unsatisfied() # unneeded?
self.apps = sorted(
list(map(self.appmgr.get, self.appnames)),
key=lambda app: app.appid,
)
def _is_missing_dep(self, dep_name: str):
return dep_name not in self.appnames
def _process_deps(self):
while True:
provided = []
for app in self.appnames:
# print(app)
provided.extend(
filter(
self._is_missing_dep,
self.appmgr.get(app).provides + self.appmgr.get(app).requires,
)
)
# print("provides round", provided)
if len(provided) == 0:
break
self.appnames.update(provided)
def _check_conflicts(self):
conflicts = []
for app in self.appnames:
# print(app)
if conflict_app_name := list(
filter(
lambda dep_name: dep_name in self.appnames,
self.appmgr.get(app).conflicts,
)
):
conflicts.append((app, conflict_app_name))
if len(conflicts):
raise AppBuilderException(
f"App conflicts for {', '.join(f'{conflict_dep[0]}: {conflict_dep[1]}' for conflict_dep in conflicts)}"
)
def _check_unsatisfied(self):
unsatisfied = []
for app in self.appnames:
if missing_dep := list(
filter(self._is_missing_dep, self.appmgr.get(app).requires)
):
unsatisfied.append((app, missing_dep))
if len(unsatisfied):
raise AppBuilderException(
f"Unsatisfied dependencies for {', '.join(f'{missing_dep[0]}: {missing_dep[1]}' for missing_dep in unsatisfied)}"
)
def get_apps_cdefs(self):
cdefs = set()
for app in self.apps:
cdefs.update(app.cdefines)
return sorted(list(cdefs))
def get_apps_of_type(self, apptype: FlipperAppType):
return sorted(
filter(lambda app: app.apptype == apptype, self.apps),
key=lambda app: app.order,
)
def get_builtin_app_folders(self):
return sorted(
set(
app._appdir
for app in filter(
lambda app: app.apptype in self.BUILTIN_APP_TYPES, self.apps
)
)
)
class ApplicationsCGenerator:
APP_TYPE_MAP = {
FlipperAppType.SERVICE: ("FlipperApplication", "FLIPPER_SERVICES"),
FlipperAppType.SYSTEM: ("FlipperApplication", "FLIPPER_SYSTEM_APPS"),
FlipperAppType.APP: ("FlipperApplication", "FLIPPER_APPS"),
FlipperAppType.PLUGIN: ("FlipperApplication", "FLIPPER_PLUGINS"),
FlipperAppType.DEBUG: ("FlipperApplication", "FLIPPER_DEBUG_APPS"),
FlipperAppType.SETTINGS: ("FlipperApplication", "FLIPPER_SETTINGS_APPS"),
FlipperAppType.STARTUP: ("FlipperOnStartHook", "FLIPPER_ON_SYSTEM_START"),
}
def __init__(self, buildset: AppBuildset):
self.buildset = buildset
def get_app_ep_forward(self, app: FlipperApplication):
if app.apptype == FlipperAppType.STARTUP:
return f"extern void {app.entry_point}();"
return f"extern int32_t {app.entry_point}(void* p);"
def get_app_descr(self, app: FlipperApplication):
if app.apptype == FlipperAppType.STARTUP:
return app.entry_point
return f"""
{{.app = {app.entry_point},
.name = "{app.name}",
.stack_size = {app.stack_size},
.icon = {f"&{app.icon}" if app.icon else "NULL"},
.flags = {'|'.join(f"FlipperApplicationFlag{flag}" for flag in app.flags)} }}"""
def generate(self):
contents = ['#include "applications.h"', "#include <assets_icons.h>"]
for apptype in self.APP_TYPE_MAP:
contents.extend(
map(self.get_app_ep_forward, self.buildset.get_apps_of_type(apptype))
)
entry_type, entry_block = self.APP_TYPE_MAP[apptype]
contents.append(f"const {entry_type} {entry_block}[] = {{")
contents.append(
",\n".join(
map(self.get_app_descr, self.buildset.get_apps_of_type(apptype))
)
)
contents.append("};")
contents.append(
f"const size_t {entry_block}_COUNT = COUNT_OF({entry_block});"
)
archive_app = self.buildset.get_apps_of_type(FlipperAppType.ARCHIVE)
if archive_app:
contents.extend(
[
self.get_app_ep_forward(archive_app[0]),
f"const FlipperApplication FLIPPER_ARCHIVE = {self.get_app_descr(archive_app[0])};",
]
)
return "\n".join(contents)

19
site_scons/fbt/util.py Normal file
View File

@@ -0,0 +1,19 @@
import SCons
from SCons.Subst import quote_spaces
import re
WINPATHSEP_RE = re.compile(r"\\([^\"'\\]|$)")
def tempfile_arg_esc_func(arg):
arg = quote_spaces(arg)
if SCons.Platform.platform_default() != "win32":
return arg
# GCC requires double Windows slashes, let's use UNIX separator
return WINPATHSEP_RE.sub(r"/\1", arg)
def wrap_tempfile(env, command):
env[command] = '${TEMPFILE("' + env[command] + '","$' + command + 'STR")}'

23
site_scons/fbt/version.py Normal file
View File

@@ -0,0 +1,23 @@
import subprocess
import datetime
def get_fast_git_version_id():
try:
version = (
subprocess.check_output(
[
"git",
"describe",
"--always",
"--dirty",
"--all",
"--long",
]
)
.strip()
.decode()
)
return (version, datetime.date.today())
except Exception as e:
print("Failed to check for git changes", e)

View File

@@ -0,0 +1,50 @@
Import("ENV")
if ENV["DEBUG"]:
ENV.Append(
CPPDEFINES=[
"FURI_DEBUG",
"NDEBUG",
],
CCFLAGS=[
"-Og",
],
)
elif ENV["COMPACT"]:
ENV.Append(
CPPDEFINES=[
"FURI_NDEBUG",
"NDEBUG",
],
CCFLAGS=[
"-Os",
],
)
else:
ENV.Append(
CPPDEFINES=[
"FURI_NDEBUG",
"NDEBUG",
],
CCFLAGS=[
"-Og",
],
)
ENV.Append(
LINKFLAGS=[
"-Tfirmware/targets/f${TARGET_HW}/${LINKER_SCRIPT}.ld",
],
)
if ENV["FIRMWARE_BUILD_CFG"] == "updater":
ENV.Append(
IMAGE_BASE_ADDRESS="0x20000000",
LINKER_SCRIPT="stm32wb55xx_ram_fw",
)
else:
ENV.Append(
IMAGE_BASE_ADDRESS="0x8000000",
LINKER_SCRIPT="stm32wb55xx_flash",
)

40
site_scons/site_init.py Normal file
View File

@@ -0,0 +1,40 @@
from SCons.Script import GetBuildFailures
import sys
import os
import atexit
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
def bf_to_str(bf):
"""Convert an element of GetBuildFailures() to a string
in a useful way."""
import SCons.Errors
if bf is None: # unknown targets product None in list
return "(unknown tgt)"
elif isinstance(bf, SCons.Errors.StopError):
return str(bf)
elif bf.node:
return str(bf.node) + ": " + bf.errstr
elif bf.filename:
return bf.filename + ": " + bf.errstr
return "unknown failure: " + bf.errstr
def display_build_status():
"""Display the build status. Called by atexit.
Here you could do all kinds of complicated things."""
bf = GetBuildFailures()
if bf:
# bf is normally a list of build failures; if an element is None,
# it's because of a target that scons doesn't know anything about.
failures_message = "\n".join(
["Failed building %s" % bf_to_str(x) for x in bf if x is not None]
)
print("*" * 10, "ERRORS", "*" * 10)
print(failures_message)
atexit.register(display_build_status)

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,72 @@
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 Exception(
f"Toolchain version is not supported. Allowed: {whitelisted_versions}, toolchain: {cc_version} "
)
def exists(env):
return True

View File

@@ -0,0 +1,61 @@
from SCons.Builder import Builder
from SCons.Action import Action
import SCons
from fbt.appmanifest import FlipperAppType, AppManager, ApplicationsCGenerator
# 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 entry in env.Glob("#/applications/*"):
if isinstance(entry, SCons.Node.FS.Dir) and not str(entry).startswith("."):
appmgr.load_manifest(entry.File("application.fam").abspath, entry.name)
def PrepareApplicationsBuild(env):
env["APPBUILD"] = env["APPMGR"].filter_apps(env["APPS"])
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"])
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}"),
),
}
)
def exists(env):
return True

View File

@@ -0,0 +1,151 @@
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 = [
"compiled/assets_icons.c",
"compiled/assets_icons.h",
]
return target, source
def proto_emitter(target, source, env):
out_path = target[0].path
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 generate(env):
env.SetDefault(
ASSETS_COMPILER="${ROOT_DIR.abspath}/scripts/assets.py",
NANOPB_COMPILER="${ROOT_DIR.abspath}/lib/nanopb/generator/nanopb_generator.py",
)
env.Append(
BUILDERS={
"IconBuilder": Builder(
action=Action(
"${PYTHON3} ${ASSETS_COMPILER} icons ${SOURCE.posix} ${TARGET.dir.posix}",
"${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,104 @@
from SCons.Builder import Builder
from SCons.Action import Action
from SCons.Script import Mkdir
def get_variant_dirname(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 = get_variant_dirname(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"],
],
)
return project_env
def AddDebugTarget(env, targetenv, force_flash=True):
pseudo_name = f"debug.{targetenv.subst('$FIRMWARE_BUILD_CFG')}.pseudo"
debug_target = env.GDBPy(
pseudo_name,
targetenv["FW_ELF"],
GDBPYOPTS='-ex "source debug/FreeRTOS/FreeRTOS.py" '
'-ex "source debug/PyCortexMDebug/PyCortexMDebug.py" '
'-ex "svd_load ${SVD_FILE}" '
'-ex "compare-sections"',
)
if force_flash:
env.Depends(debug_target, targetenv["FW_FLASH"])
env.Pseudo(pseudo_name)
env.AlwaysBuild(debug_target)
return debug_target
def generate(env):
env.AddMethod(AddFwProject)
env.AddMethod(AddDebugTarget)
env.SetDefault(
COPRO_MCU_FAMILY="STM32WB5x",
)
env.Append(
BUILDERS={
"DistBuilder": Builder(
action=Action(
'${PYTHON3} ${ROOT_DIR.abspath}/scripts/sconsdist.py copy -p ${DIST_PROJECTS} -s "${DIST_SUFFIX}" ${DIST_EXTRA}',
"${DISTCOMSTR}",
),
),
"CoproBuilder": Builder(
action=Action(
[
Mkdir("$TARGET"),
"${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} ",
],
"",
)
),
}
)
def exists(env):
return True

View File

@@ -0,0 +1,30 @@
import os
def BuildAppElf(env, app):
work_dir = env.subst("$EXT_APPS_WORK_DIR")
app_target_name = os.path.join(work_dir, app.appid)
app_alias = f"{env['FIRMWARE_BUILD_CFG']}_{app.appid}"
app_elf = env.Program(
app_target_name,
env.GlobRecursive("*.c*", os.path.join(work_dir, app._appdir)),
APP_ENTRY=app.entry_point,
)
app_elf_dump = env.ObjDump(app_target_name)
env.Alias(f"{app_alias}_list", app_elf_dump)
app_stripped_elf = env.ELFStripper(
os.path.join(env.subst("$PLUGIN_ELF_DIR"), app.appid), app_elf
)
env.Alias(app_alias, app_stripped_elf)
return app_stripped_elf
def generate(env, **kw):
env.SetDefault(EXT_APPS_WORK_DIR=kw.get("EXT_APPS_WORK_DIR", ".extapps"))
env.VariantDir(env.subst("$EXT_APPS_WORK_DIR"), ".", duplicate=False)
env.AddMethod(BuildAppElf)
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,52 @@
from SCons.Builder import Builder
from SCons.Action import Action
import SCons
__OBJCOPY_ARM_BIN = "arm-none-eabi-objcopy"
def generate(env):
env.SetDefault(
BIN2DFU="${ROOT_DIR.abspath}/scripts/bin2dfu.py",
OBJCOPY=__OBJCOPY_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")

View File

@@ -0,0 +1,33 @@
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
)
env.Append(
BUILDERS={
"GDB": Builder(
action=Action(
"${GDBCOM}",
"${GDBCOMSTR}",
),
),
"GDBPy": Builder(
action=Action(
"${GDBPYCOM}",
"${GDBPYCOMSTR}",
),
),
}
)
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,48 @@
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}",
"${OOCDCOMSTR}",
)
def generate(env):
env.SetDefault(
OPENOCD=__OPENOCD_BIN,
OPENOCD_OPTS="",
OPENOCD_COMMAND="",
OOCDCOMSTR="",
)
env.Append(
BUILDERS={
"OOCDFlashCommand": Builder(
action=[
_oocd_action,
Touch("${TARGET}"),
],
suffix=".flash",
src_suffix=".bin",
),
"OOCDCommand": Builder(
action=_oocd_action,
),
}
)
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,38 @@
import posixpath
import os
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):
print(f"Cannot build module {module}: scons file not found")
Exit(2)
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 generate(env):
env.AddMethod(BuildModule)
env.AddMethod(BuildModules)
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