[FL-2859,2838] fbt: improvements for FAPs (#1813)
* fbt: assets builder for apps WIP * fbt: automatically building private fap assets * docs: details on how to use image assets * fbt: renamed fap_assets -> fap_icons * fbt: support for fap_extbuild field * docs: info on fap_extbuild * fbt: added --proxy-env parame ter * fbt: made firmware_cdb & updater_cdb targets always available * fbt: renamed fap_icons -> fap_icon_assets * fbt: deprecated firmware_* target names for faps; new alias is "fap_APPID" * fbt: changed intermediate file locations for external apps * fbt: support for fap_private_libs; docs: updates * restored mbedtls as global lib * scripts: lint.py: skip "lib" subfolder * fbt: Sanity checks for building advanced faps as part of fw * docs: info on fap_private_libs; fbt: optimized *.fam indexing * fbt: cleanup; samples: added sample_icons app * fbt: moved example app to applications/examples * linter fix * docs: readme fixes * added applications/examples/application.fam stub * docs: more info on private libs Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
@@ -34,6 +34,14 @@ AddOption(
|
||||
help="List of applications to forcefully build as standalone .elf",
|
||||
)
|
||||
|
||||
AddOption(
|
||||
"--proxy-env",
|
||||
action="store",
|
||||
dest="proxy_env",
|
||||
default="",
|
||||
help="Comma-separated list of additional environment variables to pass to child SCons processes",
|
||||
)
|
||||
|
||||
|
||||
# Construction environment variables
|
||||
|
||||
@@ -230,6 +238,7 @@ vars.Add(
|
||||
("applications/system", False),
|
||||
("applications/debug", False),
|
||||
("applications/plugins", False),
|
||||
("applications/examples", False),
|
||||
("applications_user", False),
|
||||
],
|
||||
)
|
||||
|
@@ -12,14 +12,20 @@ forward_os_env = {
|
||||
"PATH": os.environ["PATH"],
|
||||
}
|
||||
# Proxying CI environment to child processes & scripts
|
||||
for env_value_name in (
|
||||
variables_to_forward = [
|
||||
"WORKFLOW_BRANCH_OR_TAG",
|
||||
"DIST_SUFFIX",
|
||||
"HOME",
|
||||
"APPDATA",
|
||||
"PYTHONHOME",
|
||||
"PYTHONNOUSERSITE",
|
||||
):
|
||||
"TMP",
|
||||
"TEMP",
|
||||
]
|
||||
if proxy_env := GetOption("proxy_env"):
|
||||
variables_to_forward.extend(proxy_env.split(","))
|
||||
|
||||
for env_value_name in variables_to_forward:
|
||||
if environ_value := os.environ.get(env_value_name, None):
|
||||
forward_os_env[env_value_name] = environ_value
|
||||
|
||||
|
@@ -1,10 +1,23 @@
|
||||
from SCons.Errors import UserError
|
||||
|
||||
|
||||
Import("ENV")
|
||||
|
||||
|
||||
from fbt.appmanifest import FlipperAppType
|
||||
|
||||
appenv = ENV.Clone(
|
||||
tools=[("fbt_extapps", {"EXT_APPS_WORK_DIR": ENV.subst("${BUILD_DIR}/.extapps")})]
|
||||
tools=[
|
||||
(
|
||||
"fbt_extapps",
|
||||
{
|
||||
"EXT_APPS_WORK_DIR": ENV.subst(
|
||||
"${BUILD_DIR}/.extapps",
|
||||
)
|
||||
},
|
||||
),
|
||||
"fbt_assets",
|
||||
]
|
||||
)
|
||||
|
||||
appenv.Replace(
|
||||
@@ -83,7 +96,16 @@ if extra_app_list := GetOption("extra_ext_apps"):
|
||||
if appenv["FORCE"]:
|
||||
appenv.AlwaysBuild(extapps["compact"].values())
|
||||
|
||||
Alias(appenv["FIRMWARE_BUILD_CFG"] + "_extapps", extapps["compact"].values())
|
||||
|
||||
# Deprecation stub
|
||||
def legacy_app_build_stub(**kw):
|
||||
raise UserError(f"Target name 'firmware_extapps' is deprecated, use 'faps' instead")
|
||||
|
||||
|
||||
appenv.PhonyTarget("firmware_extapps", appenv.Action(legacy_app_build_stub, None))
|
||||
|
||||
|
||||
Alias("faps", extapps["compact"].values())
|
||||
|
||||
if appsrc := appenv.subst("$APPSRC"):
|
||||
app_manifest, fap_file, app_validator = appenv.GetExtAppFromPath(appsrc)
|
||||
|
@@ -23,6 +23,22 @@ class FlipperAppType(Enum):
|
||||
|
||||
@dataclass
|
||||
class FlipperApplication:
|
||||
@dataclass
|
||||
class ExternallyBuiltFile:
|
||||
path: str
|
||||
command: str
|
||||
|
||||
@dataclass
|
||||
class Library:
|
||||
name: str
|
||||
fap_include_paths: List[str] = field(default_factory=lambda: ["."])
|
||||
sources: List[str] = field(default_factory=lambda: ["*.c*"])
|
||||
cflags: List[str] = field(default_factory=list)
|
||||
cdefines: List[str] = field(default_factory=list)
|
||||
cincludes: List[str] = field(default_factory=list)
|
||||
|
||||
PRIVATE_FIELD_PREFIX = "_"
|
||||
|
||||
appid: str
|
||||
apptype: FlipperAppType
|
||||
name: Optional[str] = ""
|
||||
@@ -45,6 +61,9 @@ class FlipperApplication:
|
||||
fap_description: str = ""
|
||||
fap_author: str = ""
|
||||
fap_weburl: str = ""
|
||||
fap_icon_assets: Optional[str] = None
|
||||
fap_extbuild: List[ExternallyBuiltFile] = field(default_factory=list)
|
||||
fap_private_libs: List[Library] = field(default_factory=list)
|
||||
# Internally used by fbt
|
||||
_appdir: Optional[object] = None
|
||||
_apppath: Optional[str] = None
|
||||
@@ -88,6 +107,12 @@ class AppManager:
|
||||
),
|
||||
)
|
||||
|
||||
def ExtFile(*args, **kw):
|
||||
return FlipperApplication.ExternallyBuiltFile(*args, **kw)
|
||||
|
||||
def Lib(*args, **kw):
|
||||
return FlipperApplication.Library(*args, **kw)
|
||||
|
||||
try:
|
||||
with open(app_manifest_path, "rt") as manifest_file:
|
||||
exec(manifest_file.read())
|
||||
|
@@ -10,8 +10,8 @@ import subprocess
|
||||
|
||||
def icons_emitter(target, source, env):
|
||||
target = [
|
||||
"compiled/assets_icons.c",
|
||||
"compiled/assets_icons.h",
|
||||
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
|
||||
@@ -99,17 +99,41 @@ def proto_ver_generator(target, source, env):
|
||||
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}',
|
||||
'${PYTHON3} "${ASSETS_COMPILER}" icons ${ICON_SRC_DIR} ${TARGET.dir} --filename ${ICON_FILE_NAME}',
|
||||
"${ICONSCOMSTR}",
|
||||
),
|
||||
emitter=icons_emitter,
|
||||
|
@@ -6,56 +6,146 @@ import SCons.Warnings
|
||||
import os
|
||||
import pathlib
|
||||
from fbt.elfmanifest import assemble_manifest_data
|
||||
from fbt.appmanifest import FlipperManifestException
|
||||
from fbt.sdk import SdkCache
|
||||
import itertools
|
||||
|
||||
from site_scons.fbt.appmanifest import FlipperApplication
|
||||
|
||||
|
||||
def BuildAppElf(env, app):
|
||||
work_dir = env.subst("$EXT_APPS_WORK_DIR")
|
||||
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_alias = f"{env['FIRMWARE_BUILD_CFG']}_{app.appid}"
|
||||
app_original_elf = os.path.join(work_dir, f"{app.appid}_d")
|
||||
app_sources = list(
|
||||
itertools.chain.from_iterable(
|
||||
env.GlobRecursive(source_type, os.path.join(work_dir, app._appdir.relpath))
|
||||
app_env.GlobRecursive(
|
||||
source_type,
|
||||
app_work_dir,
|
||||
exclude="lib",
|
||||
)
|
||||
for source_type in app.sources
|
||||
)
|
||||
)
|
||||
app_elf_raw = env.Program(
|
||||
app_original_elf,
|
||||
app_sources,
|
||||
APP_ENTRY=app.entry_point,
|
||||
LIBS=env["LIBS"] + app.fap_libs,
|
||||
|
||||
app_env.Append(
|
||||
LIBS=[*app.fap_libs, *private_libs],
|
||||
CPPPATH=env.Dir(app_work_dir),
|
||||
)
|
||||
|
||||
app_elf_dump = env.ObjDump(app_elf_raw)
|
||||
env.Alias(f"{app_alias}_list", app_elf_dump)
|
||||
app_elf_raw = app_env.Program(
|
||||
os.path.join(app_work_dir, f"{app.appid}_d"),
|
||||
app_sources,
|
||||
APP_ENTRY=app.entry_point,
|
||||
)
|
||||
|
||||
app_elf_augmented = env.EmbedAppMetadata(
|
||||
os.path.join(env.subst("$PLUGIN_ELF_DIR"), app.appid),
|
||||
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 = vars(app)
|
||||
manifest_vals = {
|
||||
k: v for k, v in manifest_vals.items() if k not in ("_appdir", "_apppath")
|
||||
k: v
|
||||
for k, v in vars(app).items()
|
||||
if not k.startswith(FlipperApplication.PRIVATE_FIELD_PREFIX)
|
||||
}
|
||||
|
||||
env.Depends(
|
||||
app_env.Depends(
|
||||
app_elf_augmented,
|
||||
[env["SDK_DEFINITION"], env.Value(manifest_vals)],
|
||||
[app_env["SDK_DEFINITION"], app_env.Value(manifest_vals)],
|
||||
)
|
||||
if app.fap_icon:
|
||||
env.Depends(
|
||||
app_env.Depends(
|
||||
app_elf_augmented,
|
||||
env.File(f"{app._apppath}/{app.fap_icon}"),
|
||||
app_env.File(f"{app._apppath}/{app.fap_icon}"),
|
||||
)
|
||||
env.Alias(app_alias, app_elf_augmented)
|
||||
|
||||
app_elf_import_validator = env.ValidateAppImports(app_elf_augmented)
|
||||
env.AlwaysBuild(app_elf_import_validator)
|
||||
env.Alias(app_alias, app_elf_import_validator)
|
||||
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)
|
||||
|
||||
|
||||
@@ -101,9 +191,15 @@ def GetExtAppFromPath(env, app_dir):
|
||||
appmgr = env["APPMGR"]
|
||||
|
||||
app = None
|
||||
for dir_part in reversed(pathlib.Path(app_dir).parts):
|
||||
if app := appmgr.find_by_appdir(dir_part):
|
||||
break
|
||||
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}")
|
||||
|
||||
@@ -120,7 +216,7 @@ def GetExtAppFromPath(env, app_dir):
|
||||
|
||||
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.VariantDir(env.subst("$EXT_APPS_WORK_DIR"), env.Dir("#"), duplicate=False)
|
||||
|
||||
env.AddMethod(BuildAppElf)
|
||||
env.AddMethod(GetExtAppFromPath)
|
||||
|
Reference in New Issue
Block a user