[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

0
scripts/fbt/__init__.py Normal file
View File

316
scripts/fbt/appmanifest.py Normal file
View File

@@ -0,0 +1,316 @@
from dataclasses import dataclass, field
from typing import List, Optional, Tuple
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:
@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] = ""
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
sdk_headers: List[str] = field(default_factory=list)
# .fap-specific
sources: List[str] = field(default_factory=lambda: ["*.c*"])
fap_version: Tuple[int] = field(default_factory=lambda: (0, 1))
fap_icon: Optional[str] = None
fap_libs: List[str] = field(default_factory=list)
fap_category: str = ""
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
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 find_by_appdir(self, appdir: str):
for app in self.known_apps.values():
if app._appdir.name == appdir:
return app
return None
def load_manifest(self, app_manifest_path: str, app_dir_node: object):
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_node,
_apppath=os.path.dirname(app_manifest_path),
),
)
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())
except Exception as e:
raise FlipperManifestException(
f"Failed parsing manifest '{app_manifest_path}' : {e}"
)
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_sdk_headers(self):
sdk_headers = []
for app in self.apps:
sdk_headers.extend([app._appdir.File(header) for header in app.sdk_headers])
return sdk_headers
def get_apps_of_type(self, apptype: FlipperAppType, all_known: bool = False):
return sorted(
filter(
lambda app: app.apptype == apptype,
self.appmgr.known_apps.values() if all_known else self.apps,
),
key=lambda app: app.order,
)
def get_builtin_apps(self):
return list(
filter(lambda app: app.apptype in self.BUILTIN_APP_TYPES, self.apps)
)
def get_builtin_app_folders(self):
return sorted(
set(
(app._appdir, source_type)
for app in self.get_builtin_apps()
for source_type in app.sources
)
)
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, autorun_app: str = ""):
self.buildset = buildset
self.autorun = autorun_app
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>",
f'const char* FLIPPER_AUTORUN_APP_NAME = "{self.autorun}";',
]
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)

View File

@@ -0,0 +1,85 @@
from dataclasses import dataclass
import os
import struct
from dataclasses import dataclass, field
from .appmanifest import FlipperApplication
_MANIFEST_MAGIC = 0x52474448
@dataclass
class ElfManifestBaseHeader:
manifest_version: int
api_version: int
hardware_target_id: int
manifest_magic: int = 0x52474448
def as_bytes(self):
return struct.pack(
"<IIIh",
self.manifest_magic,
self.manifest_version,
self.api_version,
self.hardware_target_id,
)
@dataclass
class ElfManifestV1:
stack_size: int
app_version: int
name: str = ""
icon: bytes = field(default=b"")
def as_bytes(self):
return struct.pack(
"<hI32s?32s",
self.stack_size,
self.app_version,
bytes(self.name.encode("ascii")),
bool(self.icon),
self.icon,
)
def assemble_manifest_data(
app_manifest: FlipperApplication,
hardware_target: int,
sdk_version,
):
image_data = b""
if app_manifest.fap_icon:
from flipper.assets.icon import file2image
image = file2image(os.path.join(app_manifest._apppath, app_manifest.fap_icon))
if (image.width, image.height) != (10, 10):
raise ValueError(
f"Flipper app icon must be 10x10 pixels, but {image.width}x{image.height} was given"
)
if len(image.data) > 32:
raise ValueError(
f"Flipper app icon must be 32 bytes or less, but {len(image.data)} bytes were given"
)
image_data = image.data
app_version_as_int = ((app_manifest.fap_version[0] & 0xFFFF) << 16) | (
app_manifest.fap_version[1] & 0xFFFF
)
data = ElfManifestBaseHeader(
manifest_version=1,
api_version=sdk_version,
hardware_target_id=hardware_target,
).as_bytes()
data += ElfManifestV1(
stack_size=app_manifest.stack_size,
app_version=app_version_as_int,
name=app_manifest.name,
icon=image_data,
).as_bytes()
return data

519
scripts/fbt/sdk.py Normal file
View File

@@ -0,0 +1,519 @@
import operator
import os
import csv
import operator
from enum import Enum, auto
from typing import List, Set, ClassVar, Any
from dataclasses import dataclass, field
from cxxheaderparser.parser import CxxParser
# 'Fixing' complaints about typedefs
CxxParser._fundamentals.discard("wchar_t")
from cxxheaderparser.types import (
EnumDecl,
Field,
ForwardDecl,
FriendDecl,
Function,
Method,
Typedef,
UsingAlias,
UsingDecl,
Variable,
Pointer,
Type,
PQName,
NameSpecifier,
FundamentalSpecifier,
Parameter,
Array,
Value,
Token,
FunctionType,
)
from cxxheaderparser.parserstate import (
State,
EmptyBlockState,
ClassBlockState,
ExternBlockState,
NamespaceBlockState,
)
@dataclass(frozen=True)
class ApiEntryFunction:
name: str
returns: str
params: str
csv_type: ClassVar[str] = "Function"
def dictify(self):
return dict(name=self.name, type=self.returns, params=self.params)
@dataclass(frozen=True)
class ApiEntryVariable:
name: str
var_type: str
csv_type: ClassVar[str] = "Variable"
def dictify(self):
return dict(name=self.name, type=self.var_type, params=None)
@dataclass(frozen=True)
class ApiHeader:
name: str
csv_type: ClassVar[str] = "Header"
def dictify(self):
return dict(name=self.name, type=None, params=None)
@dataclass
class ApiEntries:
# These are sets, to avoid creating duplicates when we have multiple
# declarations with same signature
functions: Set[ApiEntryFunction] = field(default_factory=set)
variables: Set[ApiEntryVariable] = field(default_factory=set)
headers: Set[ApiHeader] = field(default_factory=set)
class SymbolManager:
def __init__(self):
self.api = ApiEntries()
self.name_hashes = set()
# Calculate hash of name and raise exception if it already is in the set
def _name_check(self, name: str):
name_hash = gnu_sym_hash(name)
if name_hash in self.name_hashes:
raise Exception(f"Hash collision on {name}")
self.name_hashes.add(name_hash)
def add_function(self, function_def: ApiEntryFunction):
if function_def in self.api.functions:
return
self._name_check(function_def.name)
self.api.functions.add(function_def)
def add_variable(self, variable_def: ApiEntryVariable):
if variable_def in self.api.variables:
return
self._name_check(variable_def.name)
self.api.variables.add(variable_def)
def add_header(self, header: str):
self.api.headers.add(ApiHeader(header))
def gnu_sym_hash(name: str):
h = 0x1505
for c in name:
h = (h << 5) + h + ord(c)
return str(hex(h))[-8:]
class SdkCollector:
def __init__(self):
self.symbol_manager = SymbolManager()
def add_header_to_sdk(self, header: str):
self.symbol_manager.add_header(header)
def process_source_file_for_sdk(self, file_path: str):
visitor = SdkCxxVisitor(self.symbol_manager)
with open(file_path, "rt") as f:
content = f.read()
parser = CxxParser(file_path, content, visitor, None)
parser.parse()
def get_api(self):
return self.symbol_manager.api
def stringify_array_dimension(size_descr):
if not size_descr:
return ""
return stringify_descr(size_descr)
def stringify_array_descr(type_descr):
assert isinstance(type_descr, Array)
return (
stringify_descr(type_descr.array_of),
stringify_array_dimension(type_descr.size),
)
def stringify_descr(type_descr):
if isinstance(type_descr, (NameSpecifier, FundamentalSpecifier)):
return type_descr.name
elif isinstance(type_descr, PQName):
return "::".join(map(stringify_descr, type_descr.segments))
elif isinstance(type_descr, Pointer):
# Hack
if isinstance(type_descr.ptr_to, FunctionType):
return stringify_descr(type_descr.ptr_to)
return f"{stringify_descr(type_descr.ptr_to)}*"
elif isinstance(type_descr, Type):
return (
f"{'const ' if type_descr.const else ''}"
f"{'volatile ' if type_descr.volatile else ''}"
f"{stringify_descr(type_descr.typename)}"
)
elif isinstance(type_descr, Parameter):
return stringify_descr(type_descr.type)
elif isinstance(type_descr, Array):
# Hack for 2d arrays
if isinstance(type_descr.array_of, Array):
argtype, dimension = stringify_array_descr(type_descr.array_of)
return (
f"{argtype}[{stringify_array_dimension(type_descr.size)}][{dimension}]"
)
return f"{stringify_descr(type_descr.array_of)}[{stringify_array_dimension(type_descr.size)}]"
elif isinstance(type_descr, Value):
return " ".join(map(stringify_descr, type_descr.tokens))
elif isinstance(type_descr, FunctionType):
return f"{stringify_descr(type_descr.return_type)} (*)({', '.join(map(stringify_descr, type_descr.parameters))})"
elif isinstance(type_descr, Token):
return type_descr.value
elif type_descr is None:
return ""
else:
raise Exception("unsupported type_descr: %s" % type_descr)
class SdkCxxVisitor:
def __init__(self, symbol_manager: SymbolManager):
self.api = symbol_manager
def on_variable(self, state: State, v: Variable) -> None:
if not v.extern:
return
self.api.add_variable(
ApiEntryVariable(
stringify_descr(v.name),
stringify_descr(v.type),
)
)
def on_function(self, state: State, fn: Function) -> None:
if fn.inline or fn.has_body:
return
self.api.add_function(
ApiEntryFunction(
stringify_descr(fn.name),
stringify_descr(fn.return_type),
", ".join(map(stringify_descr, fn.parameters))
+ (", ..." if fn.vararg else ""),
)
)
def on_define(self, state: State, content: str) -> None:
pass
def on_pragma(self, state: State, content: str) -> None:
pass
def on_include(self, state: State, filename: str) -> None:
pass
def on_empty_block_start(self, state: EmptyBlockState) -> None:
pass
def on_empty_block_end(self, state: EmptyBlockState) -> None:
pass
def on_extern_block_start(self, state: ExternBlockState) -> None:
pass
def on_extern_block_end(self, state: ExternBlockState) -> None:
pass
def on_namespace_start(self, state: NamespaceBlockState) -> None:
pass
def on_namespace_end(self, state: NamespaceBlockState) -> None:
pass
def on_forward_decl(self, state: State, fdecl: ForwardDecl) -> None:
pass
def on_typedef(self, state: State, typedef: Typedef) -> None:
pass
def on_using_namespace(self, state: State, namespace: List[str]) -> None:
pass
def on_using_alias(self, state: State, using: UsingAlias) -> None:
pass
def on_using_declaration(self, state: State, using: UsingDecl) -> None:
pass
def on_enum(self, state: State, enum: EnumDecl) -> None:
pass
def on_class_start(self, state: ClassBlockState) -> None:
pass
def on_class_field(self, state: State, f: Field) -> None:
pass
def on_class_method(self, state: ClassBlockState, method: Method) -> None:
pass
def on_class_friend(self, state: ClassBlockState, friend: FriendDecl) -> None:
pass
def on_class_end(self, state: ClassBlockState) -> None:
pass
@dataclass(frozen=True)
class SdkVersion:
major: int = 0
minor: int = 0
csv_type: ClassVar[str] = "Version"
def __str__(self) -> str:
return f"{self.major}.{self.minor}"
def as_int(self) -> int:
return ((self.major & 0xFFFF) << 16) | (self.minor & 0xFFFF)
@staticmethod
def from_str(s: str) -> "SdkVersion":
major, minor = s.split(".")
return SdkVersion(int(major), int(minor))
def dictify(self) -> dict:
return dict(name=str(self), type=None, params=None)
class VersionBump(Enum):
NONE = auto()
MAJOR = auto()
MINOR = auto()
class ApiEntryState(Enum):
PENDING = "?"
APPROVED = "+"
DISABLED = "-"
# Special value for API version entry so users have less incentive to edit it
VERSION_PENDING = "v"
# Class that stores all known API entries, both enabled and disabled.
# Also keeps track of API versioning
# Allows comparison and update from newly-generated API
class SdkCache:
CSV_FIELD_NAMES = ("entry", "status", "name", "type", "params")
def __init__(self, cache_file: str, load_version_only=False):
self.cache_file_name = cache_file
self.version = SdkVersion(0, 0)
self.sdk = ApiEntries()
self.disabled_entries = set()
self.new_entries = set()
self.loaded_dirty_version = False
self.version_action = VersionBump.NONE
self._load_version_only = load_version_only
self.load_cache()
def is_buildable(self) -> bool:
return (
self.version != SdkVersion(0, 0)
and self.version_action == VersionBump.NONE
and not self._have_pending_entries()
)
def _filter_enabled(self, sdk_entries):
return sorted(
filter(lambda e: e not in self.disabled_entries, sdk_entries),
key=operator.attrgetter("name"),
)
def get_valid_names(self):
syms = set(map(lambda e: e.name, self.get_functions()))
syms.update(map(lambda e: e.name, self.get_variables()))
return syms
def get_functions(self):
return self._filter_enabled(self.sdk.functions)
def get_variables(self):
return self._filter_enabled(self.sdk.variables)
def get_headers(self):
return self._filter_enabled(self.sdk.headers)
def _get_entry_status(self, entry) -> str:
if entry in self.disabled_entries:
return ApiEntryState.DISABLED
elif entry in self.new_entries:
if isinstance(entry, SdkVersion):
return ApiEntryState.VERSION_PENDING
return ApiEntryState.PENDING
else:
return ApiEntryState.APPROVED
def _format_entry(self, obj):
obj_dict = obj.dictify()
obj_dict.update(
dict(
entry=obj.csv_type,
status=self._get_entry_status(obj).value,
)
)
return obj_dict
def save(self) -> None:
if self._load_version_only:
raise Exception("Only SDK version was loaded, cannot save")
if self.version_action == VersionBump.MINOR:
self.version = SdkVersion(self.version.major, self.version.minor + 1)
elif self.version_action == VersionBump.MAJOR:
self.version = SdkVersion(self.version.major + 1, 0)
if self._have_pending_entries():
self.new_entries.add(self.version)
print(
f"API version is still WIP: {self.version}. Review the changes and re-run command."
)
print(f"Entries to review:")
print(
"\n".join(
map(
str,
filter(
lambda e: not isinstance(e, SdkVersion), self.new_entries
),
)
)
)
else:
print(f"API version {self.version} is up to date")
regenerate_csv = (
self.loaded_dirty_version
or self._have_pending_entries()
or self.version_action != VersionBump.NONE
)
if regenerate_csv:
str_cache_entries = [self.version]
name_getter = operator.attrgetter("name")
str_cache_entries.extend(sorted(self.sdk.headers, key=name_getter))
str_cache_entries.extend(sorted(self.sdk.functions, key=name_getter))
str_cache_entries.extend(sorted(self.sdk.variables, key=name_getter))
with open(self.cache_file_name, "wt", newline="") as f:
writer = csv.DictWriter(f, fieldnames=SdkCache.CSV_FIELD_NAMES)
writer.writeheader()
for entry in str_cache_entries:
writer.writerow(self._format_entry(entry))
def _process_entry(self, entry_dict: dict) -> None:
entry_class = entry_dict["entry"]
entry_status = entry_dict["status"]
entry_name = entry_dict["name"]
entry = None
if entry_class == SdkVersion.csv_type:
self.version = SdkVersion.from_str(entry_name)
if entry_status == ApiEntryState.VERSION_PENDING.value:
self.loaded_dirty_version = True
elif entry_class == ApiHeader.csv_type:
self.sdk.headers.add(entry := ApiHeader(entry_name))
elif entry_class == ApiEntryFunction.csv_type:
self.sdk.functions.add(
entry := ApiEntryFunction(
entry_name,
entry_dict["type"],
entry_dict["params"],
)
)
elif entry_class == ApiEntryVariable.csv_type:
self.sdk.variables.add(
entry := ApiEntryVariable(entry_name, entry_dict["type"])
)
else:
print(entry_dict)
raise Exception("Unknown entry type: %s" % entry_class)
if entry is None:
return
if entry_status == ApiEntryState.DISABLED.value:
self.disabled_entries.add(entry)
elif entry_status == ApiEntryState.PENDING.value:
self.new_entries.add(entry)
def load_cache(self) -> None:
if not os.path.exists(self.cache_file_name):
raise Exception(
f"Cannot load symbol cache '{self.cache_file_name}'! File does not exist"
)
with open(self.cache_file_name, "rt") as f:
reader = csv.DictReader(f)
for row in reader:
self._process_entry(row)
if self._load_version_only and row.get("entry") == SdkVersion.csv_type:
break
def _have_pending_entries(self) -> bool:
return any(
filter(
lambda e: not isinstance(e, SdkVersion),
self.new_entries,
)
)
def sync_sets(
self, known_set: Set[Any], new_set: Set[Any], update_version: bool = True
):
new_entries = new_set - known_set
if new_entries:
print(f"New: {new_entries}")
known_set |= new_entries
self.new_entries |= new_entries
if update_version and self.version_action == VersionBump.NONE:
self.version_action = VersionBump.MINOR
removed_entries = known_set - new_set
if removed_entries:
print(f"Removed: {removed_entries}")
known_set -= removed_entries
# If any of removed entries was a part of active API, that's a major bump
if update_version and any(
filter(
lambda e: e not in self.disabled_entries
and e not in self.new_entries,
removed_entries,
)
):
self.version_action = VersionBump.MAJOR
self.disabled_entries -= removed_entries
self.new_entries -= removed_entries
def validate_api(self, api: ApiEntries) -> None:
self.sync_sets(self.sdk.headers, api.headers, False)
self.sync_sets(self.sdk.functions, api.functions)
self.sync_sets(self.sdk.variables, api.variables)

43
scripts/fbt/util.py Normal file
View File

@@ -0,0 +1,43 @@
import SCons
from SCons.Subst import quote_spaces
from SCons.Errors import StopError
import re
import os
import random
import string
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")}'
def link_dir(target_path, source_path, is_windows):
# print(f"link_dir: {target_path} -> {source_path}")
if os.path.lexists(target_path) or os.path.exists(target_path):
os.unlink(target_path)
if is_windows:
# Crete junction
import _winapi
if not os.path.isdir(source_path):
raise StopError(f"Source directory {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 single_quote(arg_list):
return " ".join(f"'{arg}'" if " " in arg else str(arg) for arg in arg_list)

30
scripts/fbt/version.py Normal file
View File

@@ -0,0 +1,30 @@
import subprocess
import datetime
from functools import cache
@cache
def get_git_commit_unix_timestamp():
return int(subprocess.check_output(["git", "show", "-s", "--format=%ct"]))
@cache
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)