Implemented multi-binary app packaging (#648)

This commit is contained in:
Ken Van Hoeylandt
2026-09-09 20:05:04 +02:00
committed by GitHub
parent 2496dea5c2
commit 8556103eb1
62 changed files with 1746 additions and 981 deletions
+2 -4
View File
@@ -22,19 +22,17 @@ runs:
run: python Buildscripts/release-sdk-posix.py release/TactilitySDK
- name: 'Test Integration Prep'
shell: bash
# The manifest.properties of our integration test uses version 0.0.0 to indicate that it is not using a normal SDK
# This way, it only works with our custom build. That means we have to create a copy of the SDK with the correct folder structure:
env:
TACTILITY_ARCH: ${{ steps.arch.outputs.value }}
run: |
TACTILITY_SDK_NAME="0.0.0-posix-$TACTILITY_ARCH"
TACTILITY_SDK_NAME="$(cat version.txt)-posix-$TACTILITY_ARCH"
mkdir -p test_sdk/$TACTILITY_SDK_NAME
cp -r release/TactilitySDK test_sdk/$TACTILITY_SDK_NAME
- name: 'Test Integration'
shell: bash
env:
TACTILITY_ARCH: ${{ steps.arch.outputs.value }}
run: cd Tests/SdkIntegration && TACTILITY_SDK_PATH=../../test_sdk python tactility.py build posix-$TACTILITY_ARCH --local-sdk
run: cd Tests/SdkIntegration && TACTILITY_SDK_PATH=../../test_sdk python tactility.py build -a posix-$TACTILITY_ARCH --local-sdk
- name: 'Upload Artifact'
uses: actions/upload-artifact@v4
with:
+2 -2
View File
@@ -35,7 +35,7 @@ runs:
# The manifest.properties of our integration test uses version 0.0.0 to indicate that it is not using a normal SDK
# This way, it only works with our custom build. That means we have to create a copy of the SDK with the correct folder structure:
run: |
TACTILITY_SDK_NAME="0.0.0-${{ inputs.arch }}"
TACTILITY_SDK_NAME="$(cat version.txt)-${{ inputs.arch }}"
mkdir -p test_sdk/$TACTILITY_SDK_NAME
cp -r release/TactilitySDK test_sdk/$TACTILITY_SDK_NAME
- name: 'Test Integration'
@@ -43,7 +43,7 @@ runs:
with:
esp_idf_version: v5.5.2
target: ${{ inputs.arch }}
command: export TACTILITY_SDK_PATH=../../test_sdk && cd Tests/SdkIntegration && python tactility.py build ${{ inputs.arch }} --local-sdk
command: export TACTILITY_SDK_PATH=../../test_sdk && cd Tests/SdkIntegration && python tactility.py build -a ${{ inputs.arch }} --local-sdk
- name: 'Upload Artifact'
uses: actions/upload-artifact@v4
with:
@@ -16,6 +16,8 @@
#include <esp_elf.h>
#include <esp_err.h>
#include <sys/stat.h>
#include <cstdio>
#include <new>
#include <string>
@@ -64,13 +66,22 @@ error_t read_file(const char* path, uint8_t** out_data, size_t* out_size) {
return ERROR_NONE;
}
// location.location can be either an app's install directory or the .elf file directly; the
// former resolves to the per-target binary at {dir}/elf/{CONFIG_IDF_TARGET}.elf.
bool is_regular_file(const std::string& path) {
struct stat path_stat {};
return ::stat(path.c_str(), &path_stat) == 0 && S_ISREG(path_stat.st_mode);
}
// location.location can be either an app's install directory or the .elf file directly. A
// "packaged" app's install directory always holds its single binary at the fixed path
// {dir}/bin/{CONFIG_IDF_TARGET}/app.elf - a "terminal" app has no such file (its several
// binaries keep their own names), so this correctly leaves it unresolvable - terminal apps
// aren't run through AppLoaderApi (see app/install.h).
std::string resolve_elf_path(const std::string& path) {
if (path.ends_with(".elf")) {
return path;
}
return path + "/elf/" + CONFIG_IDF_TARGET + ".elf";
std::string candidate = path + "/bin/" CONFIG_IDF_TARGET "/app.elf";
return is_regular_file(candidate) ? candidate : "";
}
constexpr ElfRequirements EXECUTABLE_REQUIREMENTS = {
+9
View File
@@ -9,6 +9,7 @@ tactility_add_module(app-module
PRIV_INCLUDE_DIRS private/
INCLUDE_DIRS include/
REQUIRES TactilityKernel service-module minitar
PRIV_REQUIRES TactilityKernelCpp
)
# Tells source/io.cpp its real-syscall fallback must go through __real_read/write/close()
@@ -19,3 +20,11 @@ if (NOT APPLE)
tactility_get_module_name(app-module MODULE_NAME)
target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO)
endif ()
# install.cpp resolves an installed binary's fixed bin/<platform>/<binary>.so path itself, so it
# needs the same platform-arch string app-posix-module's own CMakeLists.txt defines for its
# loader (the ESP32 equivalent, CONFIG_IDF_TARGET, comes for free from sdkconfig.h there).
if (NOT ESP_PLATFORM)
tactility_get_module_name(app-module MODULE_NAME)
target_compile_definitions(${MODULE_NAME} PRIVATE "TACTILITY_POSIX_ARCH=\"${CMAKE_SYSTEM_PROCESSOR}\"")
endif ()
+15 -14
View File
@@ -12,8 +12,8 @@ extern "C" {
/**
* Computes the install directory for @a app_id (does not check whether anything is actually
* installed there).
* @param[out] path always NULL-terminated on return, even on failure (empty string if
* @a path_size == 0 - nothing is written in that case; otherwise at least "" is written)
* @param[out] path always NULL-terminated on return, even on failure. Empty when @a path_size
* is 0, since nothing is written in that case.
* @retval ERROR_NONE on success
* @retval ERROR_BUFFER_OVERFLOW @a path_size is too small to hold the path (including the
* NULL terminator)
@@ -22,14 +22,15 @@ extern "C" {
error_t app_get_install_path(const char* app_id, char* path, size_t path_size);
/**
* Installs an app from a tarball at @a source_path: extracts it into the app install directory,
* parses the extracted manifest.properties (see app/metadata.h) to determine its id, then
* registers it with app_manager_add() as an AppLocation{APP_LOCATION_PATH, <install dir>} app.
* If an app with the same id is already installed (via a previous app_install() call), it is
* uninstalled first - stopped if running, its old install directory removed - before the new
* one takes its place.
* @param[in] source_path path to a tar file containing the app (must have manifest.properties
* at its root)
* Installs a package from a tarball at @a source_path: extracts it into the package's install
* directory, parses the extracted manifest.properties (see app/package_manifest.h) into a
* PackageManifest and one or more AppManifestBindings, then registers each with app_manager_add()
* as an AppLocation{APP_LOCATION_PATH, <binary path>} app.
* If a package with the same id is already installed (via a previous app_install() call), it is
* uninstalled first: every one of its apps stopped if running, then its old install directory
* removed, before the new one takes its place.
* @param[in] source_path path to a tar file containing the package (must have
* manifest.properties at its root)
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND @a source_path doesn't exist / can't be read
* @retval ERROR_INVALID_ARGUMENT the tarball has no valid manifest.properties at its root
@@ -37,11 +38,11 @@ error_t app_get_install_path(const char* app_id, char* path, size_t path_size);
error_t app_install(const char* source_path);
/**
* Uninstalls a previously app_install()-ed app: stops it if currently running, deletes its
* install directory, and unregisters it (app_manager_remove()).
* @param[in] app_id the id the app was installed under (AppMetadata::app_id)
* Uninstalls a previously app_install()-ed package: stops every one of its apps if currently
* running, deletes its install directory, and unregisters all of them (app_manager_remove()).
* @param[in] app_id the package id it was installed under (PackageManifest::id)
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND no such app was installed via app_install()
* @retval ERROR_NOT_FOUND no such package was installed via app_install()
*/
error_t app_uninstall(const char* app_id);
+2 -2
View File
@@ -1,11 +1,11 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include "location.h"
#include <app/manifest.h>
#include <tactility/error.h>
#include <stdbool.h>
#include <stdint.h>
#include "location.h"
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
+45 -2
View File
@@ -3,6 +3,7 @@
#include <app/instance.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/stream.h>
#include <tactility/error.h>
@@ -44,6 +45,48 @@ error_t app_manager_find_manifest(const char* id, struct AppManifest* out_manife
typedef void (*AppManifestVisitorFn)(const struct AppManifest* manifest, void* context);
void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context);
/**
* Registers a package for enumeration via app_manager_for_each_package(). Separate from
* registering its apps - the caller still calls app_manager_add() for each AppManifest.
* @param[in] app_ids the ids of the AppManifest(s) this package registered
* @retval ERROR_INVALID_ARGUMENT a package with the same id is already registered
* @retval ERROR_NONE on success
*/
error_t app_manager_add_package(const struct PackageManifest* package, const char* const* app_ids, size_t app_id_count);
/**
* Unregisters a previously-added package. Does not touch its apps' own registrations.
* @retval ERROR_NOT_FOUND no package with this id is registered
* @retval ERROR_NONE on success
*/
error_t app_manager_remove_package(const char* package_id);
/**
* @param[out] out_package set to a copy of the package on success
* @retval ERROR_NOT_FOUND no package with this id is registered
* @retval ERROR_NONE on success
*/
error_t app_manager_find_package(const char* package_id, struct PackageManifest* out_package);
/** One registered package, handed to AppPackageVisitorFn - see app_manager_for_each_package(). */
struct AppPackage {
struct PackageManifest package;
/** How many entries @a app_ids points to. */
size_t app_id_count;
/** Valid only for the duration of the app_manager_for_each_package() call that produced
* this - copy out what's needed before returning from the visitor. */
const char* const* app_ids;
};
typedef void (*AppPackageVisitorFn)(const struct AppPackage* pkg, void* context);
/**
* Calls @a visitor once for every registered package. Iteration order is unspecified.
* @warning Same threading contract as app_manager_for_each_manifest(): runs with an internal
* lock held - do not call any app_manager_*() function from inside @a visitor.
*/
void app_manager_for_each_package(AppPackageVisitorFn visitor, void* context);
/** One fd-to-stream binding for app_start_with_streams() (app/start.h). Every field is passed
* through to app_stream_subscribe() as-is; see its own doc for the ownership contracts. */
struct AppStreamBinding {
@@ -90,8 +133,8 @@ error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size);
/**
* Registers @a path as a directory to scan for app manifests - each direct subdirectory of
* @a path is expected to hold a manifest.properties (see app/metadata.h), matching the layout
* app_install() creates ({install dir}/{app_id}/manifest.properties), though this is not
* @a path is expected to hold a manifest.properties (see app/package_manifest.h), matching the
* layout app_install() creates ({install dir}/{package id}/manifest.properties), though this is not
* install/uninstall - it only ever adds/removes manifest registrations, never touches files on
* disk or running instances. No-op if @a path is already registered. Does not scan immediately -
* call app_manager_install_path_scan() to do that.
+11 -6
View File
@@ -11,7 +11,10 @@ extern "C" {
#endif
// Character count, excluding null terminator
#define APP_ID_LENGTH 32
#define APP_MANIFEST_ID_LENGTH 32
// Character count, excluding null terminator
#define APP_MANIFEST_NAME_LENGTH 32
/** Broad classification of an app, used for grouping/launcher presentation. */
enum AppCategory {
@@ -45,10 +48,10 @@ struct AppStackConfig {
/** Describes a registrable app. One manifest exists per app id. */
struct AppManifest {
/** Unique app identifier. Should never be NULL. */
const char* id;
/** Human-readable name. Should never be NULL. */
const char* name;
/** Unique app identifier. Must be NULL-terminated. */
char id[APP_MANIFEST_ID_LENGTH + 1];
/** Human-readable name. Must be NULL-terminated. */
char name[APP_MANIFEST_NAME_LENGTH + 1];
enum AppCategory category;
struct AppLocation location;
/** Bitmask of AppManifestFlags. Most apps should leave this 0. */
@@ -57,7 +60,9 @@ struct AppManifest {
struct AppStackConfig stack;
};
bool app_id_is_valid(const char* id);
bool app_manifest_id_is_valid(const char* id);
bool app_manifest_name_is_valid(const char* name);
bool app_manifest_stack_size_is_valid(const char* value);
#ifdef __cplusplus
}
-74
View File
@@ -1,74 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define APP_METADATA_TARGET_SDK_LENGTH 16
#define APP_METADATA_APP_ID_LENGTH 32
#define APP_METADATA_APP_NAME_LENGTH 32
#define APP_METADATA_APP_VERSION_NAME_LENGTH 16
#define APP_METADATA_REQUIRES_DEVICE_ID_LENGTH 64
struct AppMetadata {
/**
* The SDK version that was used to compile this app. (e.g. "0.6.0")
* Must be NULL-terminated.
*/
char target_sdk[APP_METADATA_TARGET_SDK_LENGTH + 1];
/**
* The identifier by which the app is launched by the system and other apps.
* Must be NULL-terminated.
*/
char app_id[APP_METADATA_APP_ID_LENGTH + 1];
/**
* The user-readable name of the app. Used in UI.
* Must be NULL-terminated.
*/
char app_name[APP_METADATA_APP_NAME_LENGTH + 1];
/**
* The version as it is displayed to the user (e.g. "1.2.0")
* Must be NULL-terminated.
*/
char app_version_name[APP_METADATA_APP_VERSION_NAME_LENGTH + 1];
/** The technical version (must be incremented with new releases of the app) */
uint64_t app_version_code;
/**
* Comma-separated list of device ids the app is restricted to (e.g. "m5stack-tab5"), matching
* the folder names under Devices/. Empty means unrestricted.
* Must be NULL-terminated.
*/
char requires_device_id[APP_METADATA_REQUIRES_DEVICE_ID_LENGTH + 1];
/**
* Stack depth (in words) for the app's task. Optional; 0 means scheduler default.
* @warning Avoid default values: the default is conservative, which wastes memory.
*/
uint32_t stack_depth;
};
/**
* Parses a manifest.properties file at @a path into @a out_metadata, auto-detecting the V1
* (sectioned, e.g. "[app]id=...") or V2 (flat dot-notation, e.g. "app.id=...") format from its
* first line.
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND the file doesn't exist / couldn't be opened
* @retval ERROR_INVALID_ARGUMENT the file isn't a valid manifest, or a field's value doesn't fit
* @a out_metadata's fixed-size buffers
*/
error_t app_metadata_parse(const char* path, struct AppMetadata* out_metadata);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/manifest.h>
#include <tactility/error.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define PACKAGE_MANIFEST_TARGET_SDK_LENGTH 16
#define PACKAGE_MANIFEST_ID_LENGTH 32
#define PACKAGE_MANIFEST_VERSION_NAME_LENGTH 16
#define PACKAGE_MANIFEST_REQUIRES_DEVICE_ID_LENGTH 64
/** Character count, excluding null terminator, for AppManifestBinding::binary. */
#define APP_MANIFEST_BINARY_LENGTH 31
/** Largest number of AppManifest entries package_manifest_parse() can produce from a single
* manifest.properties file. */
#define PACKAGE_MANIFEST_MAX_APP_MANIFESTS 32
/** A package-level manifest.properties: SDK/version metadata that applies to the whole package,
* not to any one of its (possibly several) apps. Not kept in memory at runtime - only used to
* create and cache the AppManifest(s) it describes. */
struct PackageManifest {
/**
* The package identifier (e.g. the install directory name). Distinct from any of its own
* AppManifest ids.
* Must be NULL-terminated.
*/
char id[PACKAGE_MANIFEST_ID_LENGTH + 1];
/**
* The package version as it is displayed to the user (e.g. "1.2.0")
* Must be NULL-terminated.
*/
char version_name[PACKAGE_MANIFEST_VERSION_NAME_LENGTH + 1];
/** The package's technical version (must be incremented with new releases). */
uint64_t version_code;
/**
* The SDK version that was used to compile this package. (e.g. "0.6.0")
* Must be NULL-terminated.
*/
char target_sdk[PACKAGE_MANIFEST_TARGET_SDK_LENGTH + 1];
/**
* Comma-separated list of device ids the package is restricted to (e.g. "m5stack-tab5"),
* matching the folder names under Devices/. Empty means unrestricted.
* Must be NULL-terminated.
*/
char requires_device_id[PACKAGE_MANIFEST_REQUIRES_DEVICE_ID_LENGTH + 1];
/** How many AppManifest entries this package's manifest.properties declared. */
uint32_t app_manifest_count;
};
/** Pairs a parsed AppManifest with the filename (without extension) it installs as under
* bin/<platform>/ - e.g. "main" resolves to bin/posix-x86_64/main.so. Not kept anywhere at
* runtime - same lifetime as PackageManifest, only used to hand parse results to the installer/
* scanner, which resolve `binary` into AppManifest::location::location. */
struct AppManifestBinding {
struct AppManifest manifest;
char binary[APP_MANIFEST_BINARY_LENGTH + 1];
};
/**
* Parses a manifest.properties file at @a path (flat dot-notation, e.g. "app.id=...") into
* @a out_package and @a out_bindings.
* @param[out] out_bindings written with up to @a bindings_capacity entries (see
* PackageManifest::app_manifest_count for how many)
* @param[in] bindings_capacity the capacity of @a out_bindings
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND the file doesn't exist / couldn't be opened
* @retval ERROR_INVALID_ARGUMENT the file isn't a valid manifest, or a field's value doesn't fit
* its fixed-size buffer
* @retval ERROR_BUFFER_OVERFLOW the manifest declares more apps than @a bindings_capacity
*/
error_t app_package_manifest_parse(const char* path, struct PackageManifest* out_package, struct AppManifestBinding* out_bindings, size_t bindings_capacity);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#include <string>
// Resolves an AppManifestBinding::binary filename (without extension) to its installed path
// under this fixed, predictable location, mirroring what each platform's own loader
// (app_posix_loader_service.cpp / app_esp32_loader_service.cpp) already resolves a plain
// ".so"/".elf" AppLocation::location straight through unchanged - so setting location.location
// to this exact path means neither loader needs to guess which of a package's several binaries
// an AppManifest refers to.
inline std::string app_resolve_binary_path(const std::string& install_dir, const std::string& binary) {
#ifdef ESP_PLATFORM
return install_dir + "/bin/" CONFIG_IDF_TARGET "/" + binary + ".elf";
#else
return install_dir + "/bin/posix-" TACTILITY_POSIX_ARCH "/" + binary + ".so";
#endif
}
@@ -3,8 +3,11 @@
#include <app/instance.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/private/fd_table.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/freertos/freertos.h>
#include <tactility/freertos/semphr.h>
@@ -13,6 +16,7 @@
#include <stdint.h>
#include <string>
#include <unordered_map>
#include <vector>
/**
* A dedicated completion signal(1) for one app instance's task, given as the
@@ -57,8 +61,16 @@ struct AppInstanceRecord {
AppFdTable fd_table {};
};
/** A registered installed package - see app_manager_add_package() (app/manager.h). */
struct AppPackageRecord {
struct PackageManifest package;
std::vector<std::string> app_ids;
};
struct AppLedger {
std::unordered_map<std::string, const AppManifest*> manifests;
// OptExternalAllocator: bigger entries than `manifests`, and unlike `instances` isn't on the app start/stop hot path.
std::unordered_map<std::string, AppPackageRecord, std::hash<std::string>, std::equal_to<std::string>, tt::OptExternalAllocator<std::pair<const std::string, AppPackageRecord>>> packages;
std::unordered_map<uint32_t, AppInstanceRecord> instances;
uint32_t next_instance_id = 1;
Mutex mutex {};
@@ -1,35 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/metadata.h>
#include <map>
#include <string>
/** Shared helpers + per-format parsers for app_metadata_parse() (source/app_metadata_parsing.cpp)
* - split out like the old tt::app manifest parser (AppManifestParsing/V1/V2.cpp) that this is
* modelled on, one file per format plus a shared dispatcher. */
bool app_metadata_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value);
bool app_metadata_is_valid_format_version(const std::string& version);
bool app_metadata_is_valid_name(const std::string& name);
bool app_metadata_is_valid_version_name(const std::string& version);
bool app_metadata_is_valid_version_code(const std::string& version);
bool app_metadata_is_valid_stack_size(const std::string& value);
/** Validates a comma-separated list of device ids (alphanumeric + '-' items, matching Devices/<id> folder names). */
bool app_metadata_is_valid_device_id_list(const std::string& value);
/** Copies @a value into @a dest (a fixed-size buffer of @a dest_size bytes, including the NULL
* terminator) if it fits.
* @retval false @a value doesn't fit in @a dest_size bytes - @a dest is left untouched */
bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string& value);
/** Parses a V1 (sectioned INI, e.g. "[app]versionName=...") manifest map into @a out_metadata. */
bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties, struct AppMetadata& out_metadata);
/** Parses a V2 (flat dot-notation, e.g. "app.version.name=...") manifest map into @a out_metadata. */
bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties, struct AppMetadata& out_metadata);
bool app_metadata_validate_string(const std::string& value, bool (*is_valid_char)(char));
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/package_manifest.h>
#include <TactilityCpp/Allocator.h>
#include <map>
#include <string>
#include <vector>
/** Shared helpers + parser for app_package_manifest_parse() (source/package_manifest_parsing.cpp). */
bool app_package_manifest_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value);
bool app_package_manifest_is_valid_format_version(const std::string& version);
bool app_package_manifest_is_valid_version_name(const std::string& version);
bool app_package_manifest_is_valid_version_code(const std::string& version);
bool app_package_manifest_is_valid_bool(const std::string& value);
/** Validates a comma-separated list of device ids (alphanumeric + '-' items, matching Devices/<id> folder names). */
bool app_package_manifest_is_valid_device_id_list(const std::string& value);
/** Validates a binary filename stem (AppManifestBinding::binary): alphanumeric plus '.', '_', '-'. */
bool app_package_manifest_is_valid_binary_name(const std::string& value);
/** Copies @a value into @a dest (a fixed-size buffer of @a dest_size bytes, including the NULL
* terminator) if it fits.
* @retval false @a value doesn't fit in @a dest_size bytes - @a dest is left untouched */
bool app_package_manifest_copy_bounded(char* dest, size_t dest_size, const std::string& value);
/** Parses a V2 (flat dot-notation, e.g. "app.version.name=...") manifest map into @a out_package
* and (unless @a bindings_capacity is 0) its single AppManifestBinding, out_bindings[0].
* @retval ERROR_NONE on success
* @retval ERROR_INVALID_ARGUMENT a required key is missing or a value doesn't fit/validate
* @retval ERROR_BUFFER_OVERFLOW @a bindings_capacity is nonzero but smaller than needed */
error_t package_manifest_parse_v2(const std::map<std::string, std::string>& properties, struct PackageManifest& out_package, struct AppManifestBinding* out_bindings, size_t bindings_capacity);
/** Parses a V3 (flat dot-notation with 0-indexed "app.N.*" blocks) manifest map into
* @a out_package and up to @a bindings_capacity AppManifestBinding entries.
* @retval ERROR_NONE on success
* @retval ERROR_INVALID_ARGUMENT a required key is missing or a value doesn't fit/validate
* @retval ERROR_BUFFER_OVERFLOW @a bindings_capacity is nonzero but smaller than the
* manifest's app_manifest_count */
error_t package_manifest_parse_v3(const std::map<std::string, std::string>& properties, struct PackageManifest& out_package, struct AppManifestBinding* out_bindings, size_t bindings_capacity);
bool app_package_manifest_validate_string(const std::string& value, bool (*is_valid_char)(char));
/** Convenience wrapper around app_package_manifest_parse(): parses @a path once to learn
* PackageManifest::app_manifest_count, then resizes @a out_bindings to fit exactly and parses
* again to fill it - so the caller never needs to pre-allocate a fixed maximum.
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND the file doesn't exist / couldn't be opened
* @retval ERROR_INVALID_ARGUMENT the file isn't a valid manifest, or a field's value doesn't fit
* its fixed-size buffer */
error_t app_package_manifest_parse_into(const char* path, struct PackageManifest& out_package, std::vector<struct AppManifestBinding, tt::OptExternalAllocator<struct AppManifestBinding>>& out_bindings);
+93 -75
View File
@@ -2,10 +2,14 @@
#include <app/install.h>
#include <app/manager.h>
#include <app/metadata.h>
#include <app/package_manifest.h>
#include <app/private/binary_path.h>
#include <app/private/fs.h>
#include <app/private/ledger.h>
#include <app/private/package_manifest_parsing.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/log.h>
@@ -29,7 +33,7 @@ constexpr auto* TAG = "app_install";
namespace {
// region Filesystem helpers (app-module may not depend upward on Tactility::file - see
// region Filesystem helpers (app-module may not depend upward on Tactility::file; see
// app_metadata_parsing.cpp for the same constraint applied to properties-file loading)
std::string last_path_segment(const std::string& path) {
@@ -164,9 +168,9 @@ bool untar(const std::string& tar_path, const std::string& destination_path) {
// endregion
// region Staging-path lock: at most one caller may clean up/populate a given staging_path at a
// time, keyed by source basename. The HTTP server and app tasks can call app_install()
// concurrently, e.g. two uploads sharing a source basename - without this, one call's cleanup
// can delete or overwrite the staging directory another call is still extracting into.
// time, keyed by source basename. The HTTP server and app tasks can call app_install_package()
// concurrently, e.g. two uploads sharing a source basename; without this lock, one call's
// cleanup could delete or overwrite the staging directory another call is still extracting into.
struct StagingLock {
Mutex mutex {};
@@ -223,18 +227,19 @@ void release_staging_lock(const std::string& path) {
// endregion
// region Installed-app registry: owns the AppManifest (and its id/name/path strings) that
// app_manager's ledger only keeps a non-owning pointer to (see app_manager_add()'s contract).
// region Installed-package registry: owns the AppManifests (and their backing location-path
// strings) that app_manager's ledger only keeps non-owning pointers to (see app_manager_add()'s
// contract). AppManifest::id/name own their own storage directly (fixed arrays), so this doesn't
// need to separately own those.
struct InstalledAppRecord {
std::string id;
std::string name;
std::string path;
AppManifest manifest {};
struct InstalledPackageRecord {
std::string path; // install directory
std::vector<AppManifest> manifests; // one per AppManifest the package declared
std::vector<std::string> locations; // backs manifests[i].location.location, same indices
};
struct InstallRegistry {
std::unordered_map<std::string, std::unique_ptr<InstalledAppRecord>> apps;
std::unordered_map<std::string, std::unique_ptr<InstalledPackageRecord>> apps;
Mutex mutex {};
InstallRegistry() { mutex_construct(&mutex); }
@@ -245,46 +250,68 @@ InstallRegistry& install_registry() {
return registry;
}
// Registers @a app_dir_path (already confirmed to hold a valid manifest.properties, parsed into
// @a metadata) with app_manager_add(), taking ownership of its id/name/path strings.
// @warning Caller must hold install_registry().mutex, and must have already ensured
// @a metadata.app_id isn't already registered (app_manager_add() rejects duplicates, but the
// InstalledAppRecord for the earlier registration would leak since this always inserts fresh).
error_t register_installed_app_locked(const std::string& app_dir_path, const AppMetadata& metadata) {
// Registers every AppManifest in @a bindings (@a count of them) with app_manager_add(), taking
// ownership of their backing location strings, then registers @a package itself.
// @warning Caller must hold install_registry().mutex.
error_t register_installed_package_locked(const PackageManifest& package, const std::string& install_path, const AppManifestBinding* bindings, size_t count) {
auto& registry = install_registry();
auto record = std::make_unique<InstalledAppRecord>();
record->id = metadata.app_id;
record->name = metadata.app_name;
record->path = app_dir_path;
record->manifest = AppManifest {
.id = record->id.c_str(),
.name = record->name.c_str(),
.category = APP_CATEGORY_USER,
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
.flags = 0,
.stack = { .depth = static_cast<uint16_t>(metadata.stack_depth), .desired_memory_capability = 0 },
};
auto record = std::make_unique<InstalledPackageRecord>();
record->path = install_path;
// Sized once, up front: manifests[i].location.location points into locations[i].c_str(),
// which would dangle if either vector reallocated afterward.
record->manifests.resize(count);
record->locations.resize(count);
// Belt-and-braces: app_install()'s earlier app_manager_remove() call is meant to have
// already cleared any stale registration for this id (e.g. left over from
// app_manager_install_path_scan()'s separate registry), but that call happens before the
// tarball is even extracted - remove once more, right before add, so a duplicate id can
// never turn a filesystem-level install success into a reported failure.
app_manager_remove(record->id.c_str());
error_t add_result = app_manager_add(&record->manifest);
if (add_result != ERROR_NONE) {
LOG_E(TAG, "Failed to register app '%s': %s", record->id.c_str(), error_to_string(add_result));
return add_result;
for (size_t i = 0; i < count; i++) {
record->manifests[i] = bindings[i].manifest;
record->locations[i] = app_resolve_binary_path(install_path, bindings[i].binary);
record->manifests[i].location = { APP_LOCATION_PATH, const_cast<char*>(record->locations[i].c_str()) };
}
registry.apps[record->id] = std::move(record);
for (size_t i = 0; i < count; i++) {
// The caller's earlier uninstall_locked() call is meant to have already cleared any stale registration for this id
// (e.g. left over from app_manager_install_path_scan()'s separate registry),
// but that call happens before the package is even extracted / the binaries moved into place; remove once more
// right before add, so a duplicate id can never turn a filesystem-level install success into a reported failure.
// Only if installed (APP_LOCATION_PATH) - never steal a built-in's id.
AppManifest existing {};
if (app_manager_find_manifest(record->manifests[i].id, &existing) == ERROR_NONE && existing.location.type == APP_LOCATION_PATH) {
app_manager_remove(record->manifests[i].id);
}
error_t add_result = app_manager_add(&record->manifests[i]);
if (add_result != ERROR_NONE) {
LOG_E(TAG, "Failed to register app '%s': %s", record->manifests[i].id, error_to_string(add_result));
// All-or-nothing: unregister whatever this package already added before failing.
for (size_t j = 0; j < i; j++) {
app_manager_remove(record->manifests[j].id);
}
return add_result;
}
}
std::vector<const char*> app_id_ptrs;
app_id_ptrs.reserve(count);
for (size_t i = 0; i < count; i++) {
app_id_ptrs.push_back(record->manifests[i].id);
}
app_manager_remove_package(package.id);
error_t add_package_result = app_manager_add_package(&package, app_id_ptrs.data(), app_id_ptrs.size());
if (add_package_result != ERROR_NONE) {
LOG_E(TAG, "Failed to register package '%s': %s", package.id, error_to_string(add_package_result));
for (size_t i = 0; i < count; i++) {
app_manager_remove(record->manifests[i].id);
}
return add_package_result;
}
registry.apps[package.id] = std::move(record);
return ERROR_NONE;
}
// Stops every currently-running instance of @a manifest. Collects matching instance ids while
// holding the ledger lock, then calls app_manager_stop() on each after releasing it - that call
// holding the ledger lock, then calls app_manager_stop() on each after releasing it: that call
// bound-joins the instance's thread, which must not happen while the ledger mutex (also taken by
// the instance's own thread_main()) is held, or the two threads would deadlock each other.
void stop_all_instances_of(const AppManifest* manifest) {
@@ -305,20 +332,22 @@ void stop_all_instances_of(const AppManifest* manifest) {
}
// Caller must already hold install_registry().mutex
error_t uninstall_locked(const std::string& app_id) {
error_t uninstall_locked(const std::string& package_id) {
auto& registry = install_registry();
auto iterator = registry.apps.find(app_id);
auto iterator = registry.apps.find(package_id);
if (iterator == registry.apps.end()) {
return ERROR_NOT_FOUND;
}
// Can't uninstall in-memory apps
if (iterator->second->manifest.location.type != APP_LOCATION_PATH) {
return ERROR_NOT_SUPPORTED;
for (const auto& manifest : iterator->second->manifests) {
// Can't uninstall in-memory apps
if (manifest.location.type != APP_LOCATION_PATH) {
continue;
}
stop_all_instances_of(&manifest);
app_manager_remove(manifest.id);
}
stop_all_instances_of(&iterator->second->manifest);
app_manager_remove(app_id.c_str());
app_manager_remove_package(package_id.c_str());
delete_recursively(iterator->second->path);
registry.apps.erase(iterator);
@@ -352,7 +381,7 @@ error_t app_get_install_path(const char* app_id, char* path, size_t path_size) {
}
error_t app_install(const char* source_path) {
LOG_I(TAG, "Installing app from %s", source_path);
LOG_I(TAG, "Installing app package from %s", source_path);
std::string app_parent_path;
if (!get_app_install_directory(app_parent_path)) {
@@ -389,8 +418,9 @@ error_t app_install(const char* source_path) {
return ERROR_INVALID_ARGUMENT;
}
AppMetadata metadata {};
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
PackageManifest package {};
std::vector<AppManifestBinding, tt::OptExternalAllocator<AppManifestBinding>> app_bindings;
if (app_package_manifest_parse_into(manifest_path.c_str(), package, app_bindings) != ERROR_NONE) {
LOG_E(TAG, "Install failed: invalid manifest");
delete_recursively(staging_path);
release_staging_lock(staging_path);
@@ -400,24 +430,13 @@ error_t app_install(const char* source_path) {
auto& registry = install_registry();
mutex_lock(&registry.mutex);
// Replace any previous install of this app id (mirrors the old install()'s "already
// running/present" handling). uninstall_locked() only clears app_install.cpp's own
// registry - the same app id may instead be registered by app_manager_install_path_scan()
// (manager.cpp's separate registry, scanning this same directory tree), which
// uninstall_locked() doesn't know about. Clear the app-manager registration unconditionally
// too, or app_manager_add() below rejects the re-add as a duplicate.
uninstall_locked(metadata.app_id);
// Replace any previous installation of this package - this also handles the app_manager
// registrations of its old AppManifests, so there's no separate app_manager_remove() needed
// here (register_installed_package_locked() below still defends against a stale registration
// per individual id, e.g. one left by app_manager_install_path_scan()'s separate registry).
uninstall_locked(package.id);
error_t remove_result = app_manager_remove(metadata.app_id);
if (remove_result != ERROR_NONE && remove_result != ERROR_NOT_FOUND) {
LOG_E(TAG, "Install failed: failed to remove existing installation");
mutex_unlock(&registry.mutex);
delete_recursively(staging_path);
release_staging_lock(staging_path);
return ERROR_RESOURCE;
}
auto final_path = app_parent_path + "/" + metadata.app_id;
auto final_path = app_parent_path + "/" + package.id;
delete_recursively(final_path);
if (rename(staging_path.c_str(), final_path.c_str()) != 0) {
@@ -429,9 +448,8 @@ error_t app_install(const char* source_path) {
}
release_staging_lock(staging_path);
// Only remaining failure mode is a duplicate id - can't happen, uninstall_locked() above
// already removed any previous registration for this exact id.
error_t add_result = register_installed_app_locked(final_path, metadata);
// app_bindings.size(), not package.app_manifest_count - the safe bound to index by.
error_t add_result = register_installed_package_locked(package, final_path, app_bindings.data(), app_bindings.size());
mutex_unlock(&registry.mutex);
return add_result;
+174 -44
View File
@@ -1,13 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/manager.h>
#include <app/metadata.h>
#include <app/package_manifest.h>
#include <app/private/arguments.h>
#include <app/private/binary_path.h>
#include <app/private/fd_table.h>
#include <app/private/fs.h>
#include <app/private/ledger.h>
#include <app/private/manager_internal.h>
#include <app/private/package_manifest_parsing.h>
#include <app/private/scheduler.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/error.h>
#include <tactility/log.h>
@@ -72,6 +76,67 @@ void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context)
mutex_unlock(&ledger.mutex);
}
error_t app_manager_add_package(const PackageManifest* package, const char* const* app_ids, size_t app_id_count) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
if (ledger.packages.contains(package->id)) {
mutex_unlock(&ledger.mutex);
LOG_E(TAG, "Package with id '%s' is already registered", package->id);
return ERROR_INVALID_ARGUMENT;
}
AppPackageRecord record { .package = *package };
record.app_ids.reserve(app_id_count);
for (size_t i = 0; i < app_id_count; i++) {
record.app_ids.emplace_back(app_ids[i]);
}
ledger.packages[package->id] = std::move(record);
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
error_t app_manager_remove_package(const char* package_id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.packages.find(package_id);
if (iterator == ledger.packages.end()) {
mutex_unlock(&ledger.mutex);
return ERROR_NOT_FOUND;
}
ledger.packages.erase(iterator);
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
error_t app_manager_find_package(const char* package_id, PackageManifest* out_package) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.packages.find(package_id);
if (iterator == ledger.packages.end()) {
mutex_unlock(&ledger.mutex);
return ERROR_NOT_FOUND;
}
*out_package = iterator->second.package;
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
void app_manager_for_each_package(AppPackageVisitorFn visitor, void* context) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
for (auto& [id, record] : ledger.packages) {
std::vector<const char*> app_id_ptrs;
app_id_ptrs.reserve(record.app_ids.size());
for (const auto& app_id : record.app_ids) {
app_id_ptrs.push_back(app_id.c_str());
}
AppPackage pkg { .package = record.package, .app_id_count = app_id_ptrs.size(), .app_ids = app_id_ptrs.data() };
visitor(&pkg, context);
}
mutex_unlock(&ledger.mutex);
}
error_t app_manager_start_internal(const AppManifest* manifest, AppLocation location, AppStackConfig stack, AppInstanceId parent_instance_id, int argc, const char* const argv_in[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) {
char** argv = app_arguments_copy(argc, argv_in);
if (argc > 0 && argv == nullptr) {
@@ -200,19 +265,19 @@ error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size) {
namespace {
// Owns the AppManifest (and its id/name/path strings) that app_manager_add() only keeps a
// non-owning pointer to. Separate from app_install.cpp's registry: scanning only
// adds/removes registrations, never touches disk or running instances.
struct ScannedAppManifest {
std::string id;
std::string name;
std::string path;
AppManifest manifest {};
// Owns the AppManifests (and their backing location-path strings) that app_manager_add() only
// keeps non-owning pointers to. Separate from app_install.cpp's registry: scanning only
// adds/removes registrations, never touches disk or running instances. AppManifest::id/name own
// their own storage directly (fixed arrays), so this doesn't need to separately own those.
struct ScannedPackageManifest {
std::string path; // scanned directory
std::vector<AppManifest> manifests; // one per AppManifest the package declared
std::vector<std::string> locations; // backs manifests[i].location.location, same indices
};
struct InstallPathRegistry {
std::vector<std::string> paths;
std::unordered_map<std::string, std::unique_ptr<ScannedAppManifest>> scanned;
std::unordered_map<std::string, std::unique_ptr<ScannedPackageManifest>> scanned;
Mutex mutex {};
InstallPathRegistry() { mutex_construct(&mutex); }
@@ -249,50 +314,69 @@ void app_manager_install_path_scan(void) {
app_fs_list_direct_subdirectories(root, found_app_dirs);
}
// Snapshot once so the rest of the scan doesn't hold registry.mutex.
// Snapshot once so the rest of the scan doesn't hold registry.mutex. Keeps each known
// package's own manifest ids too, so a package whose directory has disappeared can have all
// of its (possibly several) app_manager registrations removed below, not just one.
struct KnownPackage {
std::string path;
std::vector<std::string> manifest_ids;
};
mutex_lock(&registry.mutex);
std::unordered_map<std::string, std::string> known_paths;
std::unordered_map<std::string, KnownPackage> known_packages;
for (const auto& [id, record] : registry.scanned) {
known_paths.emplace(id, record->path);
KnownPackage known { .path = record->path };
for (const auto& manifest : record->manifests) {
known.manifest_ids.emplace_back(manifest.id);
}
known_packages.emplace(id, std::move(known));
}
mutex_unlock(&registry.mutex);
// Parses without registry.mutex held; filesystem IO is slow.
std::vector<std::unique_ptr<ScannedAppManifest>> new_records;
std::vector<std::unique_ptr<ScannedPackageManifest>> new_records;
std::vector<std::string> new_package_ids;
std::vector<PackageManifest> new_packages;
for (const auto& app_dir : found_app_dirs) {
auto manifest_path = app_dir + "/manifest.properties";
if (!app_fs_is_file(manifest_path)) {
continue;
}
AppMetadata metadata {};
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
PackageManifest package {};
// Heap-allocated (not a stack array - too large for a typical app task's stack; this
// function runs on whichever task calls app_manager_install_path_scan(), e.g. Boot's, via
// registerInstalledAppsFromFileSystems()) and sized to fit exactly, not pre-allocated to
// some fixed maximum (see app_package_manifest_parse_into()). OptExternalAllocator prefers
// PSRAM for this transient buffer, freeing up scarce internal RAM.
std::vector<AppManifestBinding, tt::OptExternalAllocator<AppManifestBinding>> app_bindings;
if (app_package_manifest_parse_into(manifest_path.c_str(), package, app_bindings) != ERROR_NONE) {
LOG_W(TAG, "Invalid manifest at %s", manifest_path.c_str());
continue;
}
if (known_paths.contains(metadata.app_id)) {
if (known_packages.contains(package.id)) {
continue;
}
auto record = std::make_unique<ScannedAppManifest>();
record->id = metadata.app_id;
record->name = metadata.app_name;
auto record = std::make_unique<ScannedPackageManifest>();
record->path = app_dir;
record->manifest = AppManifest {
.id = record->id.c_str(),
.name = record->name.c_str(),
.category = APP_CATEGORY_USER,
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
.flags = 0,
.stack = { .depth = static_cast<uint16_t>(metadata.stack_depth), .desired_memory_capability = 0 },
};
// Sized once, up front: manifests[i].location.location points into locations[i].c_str(),
// which would dangle if either vector reallocated afterward.
record->manifests.resize(app_bindings.size());
record->locations.resize(app_bindings.size());
for (size_t i = 0; i < app_bindings.size(); i++) {
record->manifests[i] = app_bindings[i].manifest;
record->locations[i] = app_resolve_binary_path(app_dir, app_bindings[i].binary);
record->manifests[i].location = { APP_LOCATION_PATH, const_cast<char*>(record->locations[i].c_str()) };
}
new_package_ids.emplace_back(package.id);
new_packages.push_back(package);
new_records.push_back(std::move(record));
}
std::vector<std::string> missing_ids;
for (const auto& [id, path] : known_paths) {
if (!app_fs_is_directory(path)) {
for (const auto& [id, known] : known_packages) {
if (!app_fs_is_directory(known.path)) {
missing_ids.push_back(id);
}
}
@@ -301,23 +385,60 @@ void app_manager_install_path_scan(void) {
// registry.mutex would fix a lock order an opposite-order caller could deadlock against.
// registry.mutex is retaken afterward only to publish the in-memory results.
for (const auto& id : missing_ids) {
app_manager_remove(id.c_str());
}
std::vector<std::unique_ptr<ScannedAppManifest>> added_records;
for (auto& record : new_records) {
if (app_manager_add(&record->manifest) == ERROR_NONE) {
added_records.push_back(std::move(record));
} else {
LOG_E(TAG, "Failed to register app %s (duplicate id?)", record->id.c_str());
for (const auto& manifest_id : known_packages.at(id).manifest_ids) {
app_manager_remove(manifest_id.c_str());
}
app_manager_remove_package(id.c_str());
}
std::vector<std::unique_ptr<ScannedPackageManifest>> added_records;
std::vector<std::string> added_package_ids;
for (size_t r = 0; r < new_records.size(); r++) {
auto& record = new_records[r];
// known_packages is only a pre-scan snapshot - two new directories can still share an id.
if (std::ranges::find(added_package_ids, new_package_ids[r]) != added_package_ids.end()) {
LOG_W(TAG, "Skipping duplicate package id %s found in this scan", new_package_ids[r].c_str());
continue;
}
size_t added_count = 0;
for (; added_count < record->manifests.size(); added_count++) {
if (app_manager_add(&record->manifests[added_count]) != ERROR_NONE) {
LOG_E(TAG, "Failed to register app %s (duplicate id?)", record->manifests[added_count].id);
break;
}
}
if (added_count != record->manifests.size()) {
// All-or-nothing: unregister whatever this package already added before failing.
for (size_t j = 0; j < added_count; j++) {
app_manager_remove(record->manifests[j].id);
}
continue;
}
std::vector<const char*> app_id_ptrs;
app_id_ptrs.reserve(record->manifests.size());
for (const auto& app_manifest : record->manifests) {
app_id_ptrs.push_back(app_manifest.id);
}
if (app_manager_add_package(&new_packages[r], app_id_ptrs.data(), app_id_ptrs.size()) != ERROR_NONE) {
LOG_E(TAG, "Failed to register package %s (duplicate id?)", new_packages[r].id);
for (const auto& app_manifest : record->manifests) {
app_manager_remove(app_manifest.id);
}
continue;
}
added_package_ids.push_back(new_package_ids[r]);
added_records.push_back(std::move(record));
}
mutex_lock(&registry.mutex);
for (const auto& id : missing_ids) {
registry.scanned.erase(id);
}
for (auto& record : added_records) {
registry.scanned[record->id] = std::move(record);
for (size_t i = 0; i < added_records.size(); i++) {
registry.scanned[added_package_ids[i]] = std::move(added_records[i]);
}
mutex_unlock(&registry.mutex);
}
@@ -332,7 +453,10 @@ error_t app_manager_install_path_uninstall(const char* app_id) {
return ERROR_NOT_FOUND;
}
const AppManifest* manifest = &iterator->second->manifest;
// Pointer, not a copy: the ledger's own AppInstanceRecord::manifest pointers (set by
// app_manager_add() from this exact vector) are compared against it by address below, same
// as the pre-existing single-manifest version of this function did.
const std::vector<AppManifest>* manifests = &iterator->second->manifests;
auto path = iterator->second->path;
mutex_unlock(&registry.mutex);
@@ -342,8 +466,11 @@ error_t app_manager_install_path_uninstall(const char* app_id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
for (const auto& [id, record] : ledger.instances) {
if (record.manifest == manifest) {
instance_ids.push_back(id);
for (const auto& manifest : *manifests) {
if (record.manifest == &manifest) {
instance_ids.push_back(id);
break;
}
}
}
mutex_unlock(&ledger.mutex);
@@ -354,7 +481,10 @@ error_t app_manager_install_path_uninstall(const char* app_id) {
// app_manager_remove() takes ledger.mutex; call outside registry.mutex too, matching
// the lock order in app_manager_install_path_scan().
app_manager_remove(app_id);
for (const auto& manifest : *manifests) {
app_manager_remove(manifest.id);
}
app_manager_remove_package(app_id);
// Delete before erasing the scan record, so a failed deletion still leaves the
// entry discoverable for a retry.
+18 -3
View File
@@ -1,15 +1,30 @@
#include <app/manifest.h>
#include <app/private/metadata_parsing_internal.h>
#include <app/private/package_manifest_parsing.h>
#include <stdlib.h>
#include <string.h>
extern "C" {
bool app_id_is_valid(const char* id) {
bool app_manifest_id_is_valid(const char* id) {
auto size = strlen(id);
return size >= 5 && size <= APP_ID_LENGTH && app_metadata_validate_string(id, [](char c) {
return size >= 5 && size <= APP_MANIFEST_ID_LENGTH && app_package_manifest_validate_string(id, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
bool app_manifest_name_is_valid(const char* name) {
auto size = strlen(name);
return size >= 2 && size <= APP_MANIFEST_NAME_LENGTH && app_package_manifest_validate_string(name, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == ' ' || c == '-';
});
}
bool app_manifest_stack_size_is_valid(const char* value) {
// 10 digits is the maximum decimal width of uint32_t.
auto size = strlen(value);
return size > 0 && size <= 10 && app_package_manifest_validate_string(value, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
}
@@ -1,190 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/metadata.h>
#include <app/private/metadata_parsing_internal.h>
#include <tactility/log.h>
#include <cctype>
#include <cstring>
#include <fstream>
#include <map>
#include <string>
constexpr auto* TAG = "app_metadata";
bool app_metadata_validate_string(const std::string& value, bool (*is_valid_char)(char)) {
for (char c: value) {
if (!is_valid_char(c)) {
return false;
}
}
return true;
}
namespace {
#define validate_string app_metadata_validate_string
std::string trim(const std::string& value) {
constexpr auto* whitespace = " \t\r\n";
auto start = value.find_first_not_of(whitespace);
if (start == std::string::npos) {
return "";
}
auto end = value.find_last_not_of(whitespace);
return value.substr(start, end - start + 1);
}
/** Validates a comma-separated list: non-empty, no leading/trailing/double commas (which would
* produce an empty item), and every item passing @a is_valid_item. */
bool validate_csv_list(const std::string& value, bool (*is_valid_item)(const std::string&)) {
if (value.empty()) {
return false;
}
size_t start = 0;
while (true) {
auto comma = value.find(',', start);
auto end = comma == std::string::npos ? value.size() : comma;
if (end == start || !is_valid_item(value.substr(start, end - start))) {
return false;
}
if (comma == std::string::npos) {
return true;
}
start = comma + 1;
}
}
/** manifest.properties format: "key=value" lines, "[section]" lines prefix every following key
* until the next section, "#" lines are comments, blank lines are skipped. Deliberately a local,
* minimal re-implementation rather than depending on Tactility's file::loadPropertiesFile() -
* app-module (like every other kernel module) may not depend upward on the Tactility layer. */
bool load_properties(const std::string& path, std::map<std::string, std::string>& out_properties, std::string& out_first_line) {
std::ifstream file(path);
if (!file.is_open()) {
return false;
}
std::string line;
std::string section_prefix;
bool got_first_line = false;
while (std::getline(file, line)) {
auto trimmed_line = trim(line);
if (trimmed_line.empty() || trimmed_line.starts_with("#")) {
continue;
}
if (!got_first_line) {
out_first_line = trimmed_line;
got_first_line = true;
}
if (trimmed_line.starts_with("[")) {
section_prefix = trimmed_line;
continue;
}
auto separator_index = trimmed_line.find('=');
if (separator_index == std::string::npos) {
LOG_E(TAG, "Failed to parse manifest line (skipped): %s", trimmed_line.c_str());
continue;
}
auto key = section_prefix + trim(trimmed_line.substr(0, separator_index));
auto value = trim(trimmed_line.substr(separator_index + 1));
out_properties[key] = value;
}
return true;
}
} // namespace
bool app_metadata_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value) {
const auto iterator = properties.find(key);
if (iterator == properties.end()) {
LOG_E(TAG, "Failed to find %s in manifest", key.c_str());
return false;
}
out_value = iterator->second;
return true;
}
bool app_metadata_is_valid_format_version(const std::string& version) {
return !version.empty() && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
bool app_metadata_is_valid_name(const std::string& name) {
return name.size() >= 2 && name.size() <= APP_METADATA_APP_NAME_LENGTH && validate_string(name, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == ' ' || c == '-';
});
}
bool app_metadata_is_valid_version_name(const std::string& version) {
return !version.empty() && version.size() <= APP_METADATA_APP_VERSION_NAME_LENGTH && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.' || c == '-' || c == '_';
});
}
bool app_metadata_is_valid_version_code(const std::string& version) {
// 20 digits is the maximum decimal width of uint64_t.
return !version.empty() && version.size() <= 20 && validate_string(version, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
bool app_metadata_is_valid_stack_size(const std::string& value) {
// 10 digits is the maximum decimal width of uint32_t.
return !value.empty() && value.size() <= 10 && validate_string(value, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
bool app_metadata_is_valid_device_id_list(const std::string& value) {
return validate_csv_list(value, [](const std::string& item) {
bool has_alnum = false;
for (char c: item) {
if (std::isalnum(static_cast<unsigned char>(c)) != 0) {
has_alnum = true;
} else if (c != '-') {
return false;
}
}
return has_alnum;
});
}
bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string& value) {
if (value.size() >= dest_size) {
return false;
}
memcpy(dest, value.c_str(), value.size() + 1);
return true;
}
error_t app_metadata_parse(const char* path, struct AppMetadata* out_metadata) {
LOG_I(TAG, "Parsing manifest %s", path);
// requires_device_id is optional in V2 and unwritten by V1; zeroing here (rather than relying
// on the caller) guarantees it reads back as empty ("unrestricted") either way.
*out_metadata = {};
std::map<std::string, std::string> properties;
std::string first_line;
if (!load_properties(path, properties, first_line)) {
LOG_E(TAG, "Failed to load manifest at %s", path);
return ERROR_NOT_FOUND;
}
// The V1 format's first line is always the literal "[manifest]" section header; V2 files are
// flat from the first line onward.
bool is_v1_format = first_line == "[manifest]";
bool success = is_v1_format
? app_metadata_parse_v1(properties, *out_metadata)
: app_metadata_parse_v2(properties, *out_metadata);
return success ? ERROR_NONE : ERROR_INVALID_ARGUMENT;
}
@@ -1,107 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include "app/manifest.h"
#include <app/metadata.h>
#include <app/private/metadata_parsing_internal.h>
#include <charconv>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v1";
bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties, AppMetadata& out_metadata) {
// [manifest]
LOG_W(TAG, "This manifest version is deprecated. Replace it with the newer version.");
std::string format_version;
if (!app_metadata_get_value(properties, "[manifest]version", format_version)) {
return false;
}
if (!app_metadata_is_valid_format_version(format_version)) {
LOG_E(TAG, "Invalid version");
return false;
}
// [app]
std::string id;
if (!app_metadata_get_value(properties, "[app]id", id)) {
return false;
}
if (!app_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid app id");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_id, sizeof(out_metadata.app_id), id)) {
LOG_E(TAG, "App id too long");
return false;
}
std::string name;
if (!app_metadata_get_value(properties, "[app]name", name)) {
return false;
}
if (!app_metadata_is_valid_name(name)) {
LOG_E(TAG, "Invalid app name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_name, sizeof(out_metadata.app_name), name)) {
LOG_E(TAG, "App name too long");
return false;
}
std::string version_name;
if (!app_metadata_get_value(properties, "[app]versionName", version_name)) {
return false;
}
if (!app_metadata_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid app version name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_version_name, sizeof(out_metadata.app_version_name), version_name)) {
LOG_E(TAG, "App version name too long");
return false;
}
std::string version_code_string;
if (!app_metadata_get_value(properties, "[app]versionCode", version_code_string)) {
return false;
}
if (!app_metadata_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid app version code");
return false;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "App version code out of range");
return false;
}
out_metadata.app_version_code = version_code; // [target]
std::string target_sdk;
if (!app_metadata_get_value(properties, "[target]sdk", target_sdk)) {
return false;
}
if (!app_metadata_copy_bounded(out_metadata.target_sdk, sizeof(out_metadata.target_sdk), target_sdk)) {
LOG_E(TAG, "Target sdk too long");
return false;
}
out_metadata.stack_depth = 0;
return true;
}
@@ -1,149 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include "app/manifest.h"
#include <app/metadata.h>
#include <app/private/metadata_parsing_internal.h>
#include <charconv>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v2";
bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties, AppMetadata& out_metadata) {
// manifest
std::string format_version;
if (!app_metadata_get_value(properties, "manifest.version", format_version)) {
return false;
}
if (!app_metadata_is_valid_format_version(format_version)) {
LOG_E(TAG, "Invalid version");
return false;
}
// app
std::string id;
if (!app_metadata_get_value(properties, "app.id", id)) {
return false;
}
if (!app_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid app id");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_id, sizeof(out_metadata.app_id), id)) {
LOG_E(TAG, "App id too long");
return false;
}
std::string name;
if (!app_metadata_get_value(properties, "app.name", name)) {
return false;
}
if (!app_metadata_is_valid_name(name)) {
LOG_E(TAG, "Invalid app name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_name, sizeof(out_metadata.app_name), name)) {
LOG_E(TAG, "App name too long");
return false;
}
std::string version_name;
if (!app_metadata_get_value(properties, "app.version.name", version_name)) {
return false;
}
if (!app_metadata_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid app version name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_version_name, sizeof(out_metadata.app_version_name), version_name)) {
LOG_E(TAG, "App version name too long");
return false;
}
std::string version_code_string;
if (!app_metadata_get_value(properties, "app.version.code", version_code_string)) {
return false;
}
if (!app_metadata_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid app version code");
return false;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "App version code out of range");
return false;
}
out_metadata.app_version_code = version_code; // [target]
// target
std::string target_sdk;
if (!app_metadata_get_value(properties, "target.sdk", target_sdk)) {
return false;
}
if (!app_metadata_copy_bounded(out_metadata.target_sdk, sizeof(out_metadata.target_sdk), target_sdk)) {
LOG_E(TAG, "Target sdk too long");
return false;
}
// requires.device.id (optional; if present, must be a non-empty comma-separated list)
auto device_id_iterator = properties.find("requires.device.id");
if (device_id_iterator != properties.end()) {
const std::string& device_id = device_id_iterator->second;
if (!app_metadata_is_valid_device_id_list(device_id)) {
LOG_E(TAG, "Invalid requires.device.id");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.requires_device_id, sizeof(out_metadata.requires_device_id), device_id)) {
LOG_E(TAG, "requires.device.id too long");
return false;
}
}
// app.stack.depth (optional; if present, must be a valid unsigned decimal fitting uint32_t)
auto stack_size_iterator = properties.find("app.stack.depth");
if (stack_size_iterator != properties.end()) {
const std::string& stack_size_string = stack_size_iterator->second;
if (!app_metadata_is_valid_stack_size(stack_size_string)) {
LOG_E(TAG, "Invalid app.stack.depth");
return false;
}
uint32_t stack_size = 0;
const auto* stack_size_first = stack_size_string.data();
const auto* stack_size_last = stack_size_first + stack_size_string.size();
if (std::from_chars(stack_size_first, stack_size_last, stack_size).ec != std::errc {}) {
LOG_E(TAG, "App stack depth out of range");
return false;
}
// Reject outright rather than truncating/clamping into AppStackConfig::depth (uint16_t) -
// a value like 1073741825 would otherwise silently narrow to 1, handing the app a
// catastrophically undersized stack instead of the huge one it declared.
if (stack_size > APP_STACK_SIZE_MAX) {
LOG_E(TAG, "App stack depth %u exceeds APP_STACK_SIZE_MAX(%u)", stack_size, APP_STACK_SIZE_MAX);
return false;
}
out_metadata.stack_depth = stack_size;
}
return true;
}
+15 -9
View File
@@ -5,7 +5,7 @@
#include <app/io.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/metadata.h>
#include <app/package_manifest.h>
#include <app/paths.h>
#include <app/scheduler.h>
#include <app/start.h>
@@ -47,20 +47,21 @@ static const ModuleSymbol SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(app_manager_for_each_manifest),
DEFINE_MODULE_SYMBOL(app_manager_add),
DEFINE_MODULE_SYMBOL(app_manager_remove),
DEFINE_MODULE_SYMBOL(app_manager_add_package),
DEFINE_MODULE_SYMBOL(app_manager_remove_package),
DEFINE_MODULE_SYMBOL(app_manager_find_package),
DEFINE_MODULE_SYMBOL(app_manager_for_each_package),
DEFINE_MODULE_SYMBOL(app_manager_get_topmost_instance_id),
DEFINE_MODULE_SYMBOL(app_manager_get_topmost_app_id),
DEFINE_MODULE_SYMBOL(app_manager_install_path_add),
DEFINE_MODULE_SYMBOL(app_manager_install_path_scan),
DEFINE_MODULE_SYMBOL(app_manager_install_path_uninstall),
// app/start
DEFINE_MODULE_SYMBOL(app_start),
DEFINE_MODULE_SYMBOL(app_start_for_result),
DEFINE_MODULE_SYMBOL(app_start_with_streams),
DEFINE_MODULE_SYMBOL(app_start_for_result_with_streams),
// app/manifest
DEFINE_MODULE_SYMBOL(app_id_is_valid),
// app/metadata
DEFINE_MODULE_SYMBOL(app_metadata_parse),
DEFINE_MODULE_SYMBOL(app_manifest_id_is_valid),
DEFINE_MODULE_SYMBOL(app_manifest_name_is_valid),
DEFINE_MODULE_SYMBOL(app_manifest_stack_size_is_valid),
// app/package_manifest
DEFINE_MODULE_SYMBOL(app_package_manifest_parse),
// app/paths
DEFINE_MODULE_SYMBOL(app_paths_get_user_data_directory),
DEFINE_MODULE_SYMBOL(app_paths_get_user_data_path),
@@ -68,6 +69,11 @@ static const ModuleSymbol SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(app_paths_get_assets_path),
// app/scheduler
DEFINE_MODULE_SYMBOL(app_scheduler_current_app_id),
// app/start
DEFINE_MODULE_SYMBOL(app_start),
DEFINE_MODULE_SYMBOL(app_start_for_result),
DEFINE_MODULE_SYMBOL(app_start_with_streams),
DEFINE_MODULE_SYMBOL(app_start_for_result_with_streams),
// app/stream
DEFINE_MODULE_SYMBOL(app_stream_subscribe),
DEFINE_MODULE_SYMBOL(app_stream_unsubscribe),
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/private/package_manifest_parsing.h>
#include <tactility/log.h>
#include <cctype>
#include <cstring>
#include <fstream>
#include <map>
#include <string>
constexpr auto* TAG = "app_metadata";
bool app_package_manifest_validate_string(const std::string& value, bool (*is_valid_char)(char)) {
for (char c: value) {
if (!is_valid_char(c)) {
return false;
}
}
return true;
}
namespace {
#define validate_string app_package_manifest_validate_string
std::string trim(const std::string& value) {
constexpr auto* whitespace = " \t\r\n";
auto start = value.find_first_not_of(whitespace);
if (start == std::string::npos) {
return "";
}
auto end = value.find_last_not_of(whitespace);
return value.substr(start, end - start + 1);
}
/** Validates a comma-separated list: non-empty, no leading/trailing/double commas (which would
* produce an empty item), and every item passing @a is_valid_item. */
bool validate_csv_list(const std::string& value, bool (*is_valid_item)(const std::string&)) {
if (value.empty()) {
return false;
}
size_t start = 0;
while (true) {
auto comma = value.find(',', start);
auto end = comma == std::string::npos ? value.size() : comma;
if (end == start || !is_valid_item(value.substr(start, end - start))) {
return false;
}
if (comma == std::string::npos) {
return true;
}
start = comma + 1;
}
}
/** manifest.properties format: flat "key=value" lines, "#" lines are comments, blank lines are
* skipped. Deliberately a local, minimal re-implementation rather than depending on Tactility's
* file::loadPropertiesFile() - app-module (like every other kernel module) may not depend upward
* on the Tactility layer. */
bool load_properties(const std::string& path, std::map<std::string, std::string>& out_properties) {
std::ifstream file(path);
if (!file.is_open()) {
return false;
}
std::string line;
while (std::getline(file, line)) {
auto trimmed_line = trim(line);
if (trimmed_line.empty() || trimmed_line.starts_with("#")) {
continue;
}
auto separator_index = trimmed_line.find('=');
if (separator_index == std::string::npos) {
LOG_E(TAG, "Failed to parse manifest line (skipped): %s", trimmed_line.c_str());
continue;
}
auto key = trim(trimmed_line.substr(0, separator_index));
auto value = trim(trimmed_line.substr(separator_index + 1));
out_properties[key] = value;
}
return true;
}
} // namespace
bool app_package_manifest_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value) {
const auto iterator = properties.find(key);
if (iterator == properties.end()) {
LOG_E(TAG, "Failed to find %s in manifest", key.c_str());
return false;
}
out_value = iterator->second;
return true;
}
bool app_package_manifest_is_valid_format_version(const std::string& version) {
return !version.empty() && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
bool app_package_manifest_is_valid_version_name(const std::string& version) {
return !version.empty() && version.size() <= PACKAGE_MANIFEST_VERSION_NAME_LENGTH && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.' || c == '-' || c == '_';
});
}
bool app_package_manifest_is_valid_version_code(const std::string& version) {
// 20 digits is the maximum decimal width of uint64_t.
return !version.empty() && version.size() <= 20 && validate_string(version, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
bool app_package_manifest_is_valid_bool(const std::string& value) {
return value == "true" || value == "false";
}
bool app_package_manifest_is_valid_device_id_list(const std::string& value) {
return validate_csv_list(value, [](const std::string& item) {
bool has_alnum = false;
for (char c: item) {
if (std::isalnum(static_cast<unsigned char>(c)) != 0) {
has_alnum = true;
} else if (c != '-') {
return false;
}
}
return has_alnum;
});
}
bool app_package_manifest_is_valid_binary_name(const std::string& value) {
return !value.empty() && value.size() <= APP_MANIFEST_BINARY_LENGTH && validate_string(value, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.' || c == '_' || c == '-';
});
}
bool app_package_manifest_copy_bounded(char* dest, size_t dest_size, const std::string& value) {
if (value.size() >= dest_size) {
return false;
}
memcpy(dest, value.c_str(), value.size() + 1);
return true;
}
error_t app_package_manifest_parse(const char* path, struct PackageManifest* out_package, struct AppManifestBinding* out_bindings, size_t bindings_capacity) {
LOG_I(TAG, "Parsing manifest %s", path);
// requires_device_id is optional; zeroing here (rather than relying on the caller) guarantees
// it reads back as empty ("unrestricted") when the manifest omits it.
*out_package = {};
for (size_t i = 0; i < bindings_capacity; i++) {
out_bindings[i] = {};
}
std::map<std::string, std::string> properties;
if (!load_properties(path, properties)) {
LOG_E(TAG, "Failed to load manifest at %s", path);
return ERROR_NOT_FOUND;
}
std::string format_version;
if (!app_package_manifest_get_value(properties, "manifest.version", format_version)) {
return ERROR_INVALID_ARGUMENT;
}
if (format_version == "0.2") {
return package_manifest_parse_v2(properties, *out_package, out_bindings, bindings_capacity);
} else if (format_version == "0.3") {
return package_manifest_parse_v3(properties, *out_package, out_bindings, bindings_capacity);
} else {
LOG_E(TAG, "Unsupported manifest.version: %s", format_version.c_str());
return ERROR_INVALID_ARGUMENT;
}
}
error_t app_package_manifest_parse_into(const char* path, PackageManifest& out_package, std::vector<AppManifestBinding, tt::OptExternalAllocator<AppManifestBinding>>& out_bindings) {
error_t result = app_package_manifest_parse(path, &out_package, nullptr, 0);
if (result != ERROR_NONE) {
return result;
}
out_bindings.resize(out_package.app_manifest_count);
result = app_package_manifest_parse(path, &out_package, out_bindings.data(), out_bindings.size());
if (result != ERROR_NONE) {
return result;
}
// Manifest could have changed between the two parses above.
if (out_package.app_manifest_count != out_bindings.size()) {
LOG_E(TAG, "Manifest at %s changed while being parsed", path);
return ERROR_INVALID_ARGUMENT;
}
return ERROR_NONE;
}
@@ -0,0 +1,172 @@
// SPDX-License-Identifier: Apache-2.0
#include "app/manifest.h"
#include <app/package_manifest.h>
#include <app/private/package_manifest_parsing.h>
#include <charconv>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v2";
error_t package_manifest_parse_v2(const std::map<std::string, std::string>& properties, PackageManifest& out_package, AppManifestBinding* out_bindings, size_t bindings_capacity) {
// manifest
std::string format_version;
if (!app_package_manifest_get_value(properties, "manifest.version", format_version)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_format_version(format_version)) {
LOG_E(TAG, "Invalid version");
return ERROR_INVALID_ARGUMENT;
}
// app
std::string id;
if (!app_package_manifest_get_value(properties, "app.id", id)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid app id");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.id, sizeof(out_package.id), id)) {
LOG_E(TAG, "App id too long");
return ERROR_INVALID_ARGUMENT;
}
std::string name;
if (!app_package_manifest_get_value(properties, "app.name", name)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_name_is_valid(name.c_str())) {
LOG_E(TAG, "Invalid app name");
return ERROR_INVALID_ARGUMENT;
}
std::string version_name;
if (!app_package_manifest_get_value(properties, "app.version.name", version_name)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid app version name");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.version_name, sizeof(out_package.version_name), version_name)) {
LOG_E(TAG, "App version name too long");
return ERROR_INVALID_ARGUMENT;
}
std::string version_code_string;
if (!app_package_manifest_get_value(properties, "app.version.code", version_code_string)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid app version code");
return ERROR_INVALID_ARGUMENT;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "App version code out of range");
return ERROR_INVALID_ARGUMENT;
}
out_package.version_code = version_code;
// target
std::string target_sdk;
if (!app_package_manifest_get_value(properties, "target.sdk", target_sdk)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.target_sdk, sizeof(out_package.target_sdk), target_sdk)) {
LOG_E(TAG, "Target sdk too long");
return ERROR_INVALID_ARGUMENT;
}
// requires.device.id (optional; if present, must be a non-empty comma-separated list)
auto device_id_iterator = properties.find("requires.device.id");
if (device_id_iterator != properties.end()) {
const std::string& device_id = device_id_iterator->second;
if (!app_package_manifest_is_valid_device_id_list(device_id)) {
LOG_E(TAG, "Invalid requires.device.id");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.requires_device_id, sizeof(out_package.requires_device_id), device_id)) {
LOG_E(TAG, "requires.device.id too long");
return ERROR_INVALID_ARGUMENT;
}
}
// A v2 manifest always describes exactly one app, sharing the package's own id.
out_package.app_manifest_count = 1;
if (bindings_capacity == 0) {
return ERROR_NONE;
}
AppManifestBinding& binding = out_bindings[0];
AppManifest& manifest = binding.manifest;
// v2 predates the "binary" key - the single app always installs as bin/<platform>/app.{elf,so}.
if (!app_package_manifest_copy_bounded(binding.binary, sizeof(binding.binary), "app")) {
LOG_E(TAG, "Binary name too long");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(manifest.id, sizeof(manifest.id), id)) {
LOG_E(TAG, "App id too long");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(manifest.name, sizeof(manifest.name), name)) {
LOG_E(TAG, "App name too long");
return ERROR_INVALID_ARGUMENT;
}
// app.stack.depth (optional; if present, must be a valid unsigned decimal fitting uint32_t)
auto stack_size_iterator = properties.find("app.stack.depth");
if (stack_size_iterator != properties.end()) {
const std::string& stack_size_string = stack_size_iterator->second;
if (!app_manifest_stack_size_is_valid(stack_size_string.c_str())) {
LOG_E(TAG, "Invalid app.stack.depth");
return ERROR_INVALID_ARGUMENT;
}
uint32_t stack_size = 0;
const auto* stack_size_first = stack_size_string.data();
const auto* stack_size_last = stack_size_first + stack_size_string.size();
if (std::from_chars(stack_size_first, stack_size_last, stack_size).ec != std::errc {}) {
LOG_E(TAG, "App stack depth out of range");
return ERROR_INVALID_ARGUMENT;
}
// Reject outright rather than truncating/clamping into AppStackConfig::depth (uint16_t) -
// a value like 1073741825 would otherwise silently narrow to 1, handing the app a
// catastrophically undersized stack instead of the huge one it declared.
if (stack_size > APP_STACK_SIZE_MAX) {
LOG_E(TAG, "App stack depth %u exceeds APP_STACK_SIZE_MAX(%u)", stack_size, APP_STACK_SIZE_MAX);
return ERROR_INVALID_ARGUMENT;
}
manifest.stack.depth = static_cast<uint16_t>(stack_size);
}
// v2 predates per-app visibility - always visible.
manifest.flags = 0;
manifest.category = APP_CATEGORY_USER;
return ERROR_NONE;
}
@@ -0,0 +1,238 @@
// SPDX-License-Identifier: Apache-2.0
#include "app/manifest.h"
#include <app/package_manifest.h>
#include <app/private/package_manifest_parsing.h>
#include <charconv>
#include <format>
#include <optional>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v3";
namespace {
// Numeric index out of an "app.<index>.<rest>" key, or nullopt if it doesn't match that shape.
std::optional<size_t> parse_app_key_index(const std::string& key) {
constexpr auto* prefix = "app.";
constexpr size_t prefix_len = 4;
if (!key.starts_with(prefix)) {
return std::nullopt;
}
auto dot = key.find('.', prefix_len);
if (dot == std::string::npos || dot == prefix_len) {
return std::nullopt;
}
size_t index = 0;
const auto* first = key.data() + prefix_len;
const auto* last = key.data() + dot;
auto result = std::from_chars(first, last, index);
if (result.ec != std::errc {} || result.ptr != last) {
return std::nullopt;
}
return index;
}
// Parses one "app.<index>.*" block into @a out_binding.
error_t parse_app_manifest(const std::map<std::string, std::string>& properties, size_t index, AppManifestBinding& out_binding) {
auto prefix = std::format("app.{}.", index);
AppManifest& out_manifest = out_binding.manifest;
std::string id;
if (!app_package_manifest_get_value(properties, prefix + "id", id)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid %sid", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_manifest.id, sizeof(out_manifest.id), id)) {
LOG_E(TAG, "%sid too long", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
std::string binary;
if (!app_package_manifest_get_value(properties, prefix + "binary", binary)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_binary_name(binary)) {
LOG_E(TAG, "Invalid %sbinary", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
// Filename (without extension) this app installs as under bin/<platform>/ - see
// app_package_manifest_parse()'s own doc.
if (!app_package_manifest_copy_bounded(out_binding.binary, sizeof(out_binding.binary), binary)) {
LOG_E(TAG, "%sbinary too long", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
std::string name;
if (!app_package_manifest_get_value(properties, prefix + "name", name)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_name_is_valid(name.c_str())) {
LOG_E(TAG, "Invalid %sname", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_manifest.name, sizeof(out_manifest.name), name)) {
LOG_E(TAG, "%sname too long", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
// <index>.stack.depth (optional; if present, must be a valid unsigned decimal fitting uint32_t)
auto stack_size_iterator = properties.find(prefix + "stack.depth");
if (stack_size_iterator != properties.end()) {
const std::string& stack_size_string = stack_size_iterator->second;
if (!app_manifest_stack_size_is_valid(stack_size_string.c_str())) {
LOG_E(TAG, "Invalid %sstack.depth", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
uint32_t stack_size = 0;
const auto* first = stack_size_string.data();
const auto* last = first + stack_size_string.size();
if (std::from_chars(first, last, stack_size).ec != std::errc {}) {
LOG_E(TAG, "%sstack.depth out of range", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
// Reject outright rather than truncating/clamping into AppStackConfig::depth (uint16_t) -
// a value like 1073741825 would otherwise silently narrow to 1, handing the app a
// catastrophically undersized stack instead of the huge one it declared.
if (stack_size > APP_STACK_SIZE_MAX) {
LOG_E(TAG, "%sstack.depth %u exceeds APP_STACK_SIZE_MAX(%u)", prefix.c_str(), stack_size, APP_STACK_SIZE_MAX);
return ERROR_INVALID_ARGUMENT;
}
out_manifest.stack.depth = static_cast<uint16_t>(stack_size);
}
// <index>.hidden (optional; defaults to false)
auto hidden_iterator = properties.find(prefix + "hidden");
if (hidden_iterator != properties.end()) {
if (!app_package_manifest_is_valid_bool(hidden_iterator->second)) {
LOG_E(TAG, "Invalid %shidden", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
out_manifest.flags = hidden_iterator->second == "true" ? APP_MANIFEST_FLAG_HIDDEN : 0;
}
out_manifest.category = APP_CATEGORY_USER;
return ERROR_NONE;
}
} // namespace
error_t package_manifest_parse_v3(const std::map<std::string, std::string>& properties, PackageManifest& out_package, AppManifestBinding* out_bindings, size_t bindings_capacity) {
// package-level fields: bare keys, no "app." prefix (that's reserved for the app.<index>.*
// blocks below).
std::string id;
if (!app_package_manifest_get_value(properties, "id", id)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid id");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.id, sizeof(out_package.id), id)) {
LOG_E(TAG, "id too long");
return ERROR_INVALID_ARGUMENT;
}
std::string version_name;
if (!app_package_manifest_get_value(properties, "version.name", version_name)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid version.name");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.version_name, sizeof(out_package.version_name), version_name)) {
LOG_E(TAG, "version.name too long");
return ERROR_INVALID_ARGUMENT;
}
std::string version_code_string;
if (!app_package_manifest_get_value(properties, "version.code", version_code_string)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid version.code");
return ERROR_INVALID_ARGUMENT;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "version.code out of range");
return ERROR_INVALID_ARGUMENT;
}
out_package.version_code = version_code;
std::string target_sdk;
if (!app_package_manifest_get_value(properties, "target.sdk", target_sdk)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.target_sdk, sizeof(out_package.target_sdk), target_sdk)) {
LOG_E(TAG, "target.sdk too long");
return ERROR_INVALID_ARGUMENT;
}
auto device_id_iterator = properties.find("requires.device.id");
if (device_id_iterator != properties.end()) {
const std::string& device_id = device_id_iterator->second;
if (!app_package_manifest_is_valid_device_id_list(device_id)) {
LOG_E(TAG, "Invalid requires.device.id");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.requires_device_id, sizeof(out_package.requires_device_id), device_id)) {
LOG_E(TAG, "requires.device.id too long");
return ERROR_INVALID_ARGUMENT;
}
}
// app.<index>.* blocks: 0-indexed, contiguous - the first missing "app.<index>.id" ends the list.
size_t count = 0;
while (properties.contains(std::format("app.{}.id", count))) {
count++;
// Bounded here, not just against the caller's bindings_capacity below: a caller that
// sizes its own buffer off PackageManifest::app_manifest_count (see
// app_package_manifest_parse_into()) would otherwise let a malicious/malformed manifest
// demand an unbounded allocation.
if (count > PACKAGE_MANIFEST_MAX_APP_MANIFESTS) {
LOG_E(TAG, "Manifest declares more than %d apps", PACKAGE_MANIFEST_MAX_APP_MANIFESTS);
return ERROR_BUFFER_OVERFLOW;
}
}
out_package.app_manifest_count = static_cast<uint32_t>(count);
// Catch a gap the count loop above would otherwise silently drop, e.g. app.0.* + app.3.*.
for (const auto& [key, value] : properties) {
auto index = parse_app_key_index(key);
if (index.has_value() && *index >= count) {
LOG_E(TAG, "Manifest declares %s but only %zu contiguous app(s) starting at app.0 were found", key.c_str(), count);
return ERROR_INVALID_ARGUMENT;
}
}
if (count == 0 || bindings_capacity == 0) {
return ERROR_NONE;
}
if (count > bindings_capacity) {
LOG_E(TAG, "Manifest declares %zu apps, capacity is %zu", count, bindings_capacity);
return ERROR_BUFFER_OVERFLOW;
}
for (size_t i = 0; i < count; i++) {
error_t result = parse_app_manifest(properties, i, out_bindings[i]);
if (result != ERROR_NONE) {
return result;
}
}
return ERROR_NONE;
}
+27 -1
View File
@@ -6,6 +6,27 @@
#include <tactility/paths.h>
#include <cstdio>
#include <string>
namespace {
// manifest.location.location is the fully-resolved binary file path
// ({install_dir}/bin/<platform>/<binary>.{elf,so} - see app_resolve_binary_path()), not the
// install directory itself - strip that fixed 3-segment suffix to recover it.
bool app_get_install_dir_from_binary_path(const char* binary_path, std::string& out_install_dir) {
std::string path = binary_path;
for (int i = 0; i < 3; i++) {
auto separator = path.find_last_of('/');
if (separator == std::string::npos) {
return false;
}
path = path.substr(0, separator);
}
out_install_dir = path;
return true;
}
} // namespace
extern "C" {
@@ -46,7 +67,12 @@ error_t app_paths_get_assets_directory(const char* app_id, char* out_path, size_
return ERROR_NOT_FOUND;
}
int written = std::snprintf(out_path, out_path_size, "%s/assets", static_cast<const char*>(manifest.location.location));
std::string install_dir;
if (!app_get_install_dir_from_binary_path(static_cast<const char*>(manifest.location.location), install_dir)) {
return ERROR_NOT_FOUND;
}
int written = std::snprintf(out_path, out_path_size, "%s/assets", install_dir.c_str());
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
+1 -1
View File
@@ -303,7 +303,7 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location,
return ERROR_OUT_OF_MEMORY;
}
// Same bound app_metadata_parse() enforces on manifest.properties-declared depths - a
// Same bound package_manifest_parse() enforces on manifest.properties-declared depths - a
// manifest built directly in C++ (not parsed from a file) must be held to it too.
if (stack.depth > APP_STACK_SIZE_MAX) {
LOG_E(TAG, "[instance %lu] stack depth %u exceeds APP_STACK_SIZE_MAX(%u)", app_instance_id, stack.depth, APP_STACK_SIZE_MAX);
@@ -73,7 +73,11 @@ bool wait_for_state(AppInstanceId id, AppInstanceState target, uint32_t timeout_
AppInstanceId start_idle_app(const char* id) {
ensure_memory_loader_registered();
AppManifest manifest { id, id, APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(idle_app_main) } };
AppManifest manifest {};
std::strncpy(manifest.id, id, sizeof(manifest.id) - 1);
std::strncpy(manifest.name, id, sizeof(manifest.name) - 1);
manifest.category = APP_CATEGORY_USER;
manifest.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(idle_app_main) };
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
AppInstanceId instance_id = 0;
REQUIRE_EQ(app_start(id, 0, nullptr, &instance_id), ERROR_NONE);
@@ -58,19 +58,21 @@ bool is_executable_file(const std::string& resolved_path) {
#endif
}
// location.location can be either an app's install directory or the .so file directly; the
// former resolves to the per-architecture binary at {dir}/elf/posix-{TACTILITY_POSIX_ARCH}.so,
// mirroring app_esp32_loader_service.cpp's resolve_elf_path().
// location.location can be either an app's install directory or the .so file directly, mirroring
// app_esp32_loader_service.cpp's resolve_elf_path(). A "packaged" app's install directory always
// holds its single binary at the fixed path {dir}/bin/posix-{TACTILITY_POSIX_ARCH}/app.so - a
// "terminal" app has no such file (its several binaries keep their own names), so this correctly
// leaves it unresolvable - terminal apps aren't run through AppLoaderApi (see app/install.h).
error_t resolve_app_path(const std::string& path, std::string& resolvedPath) {
if (path.ends_with(".so")) {
resolvedPath = path;
return ERROR_NONE;
}
std::string shared_object_path = path + "/elf/posix-" TACTILITY_POSIX_ARCH ".so";
if (!is_regular_file(shared_object_path)) {
std::string candidate = path + "/bin/posix-" TACTILITY_POSIX_ARCH "/app.so";
if (!is_regular_file(candidate)) {
return ERROR_NOT_FOUND;
}
resolvedPath = shared_object_path;
resolvedPath = candidate;
return ERROR_NONE;
}
@@ -94,9 +96,6 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
LOG_I(TAG, "Loading %s", app_path.c_str());
// RTLD_NOW: a missing symbol fails here, not mid-run(). RTLD_LOCAL: this app's own exported
// symbols (if any beyond its entry point) don't leak into the process's global scope and
// clash with a different app's.
void* handle = dlopen(app_path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (handle == nullptr) {
LOG_E(TAG, "dlopen(%s) failed: %s", app_path.c_str(), dlerror());
@@ -117,8 +116,8 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
int32_t api_run(AppRuntime runtime_ptr, uint32_t /*app_instance_id*/, int argc, char* argv[]) {
auto* runtime = static_cast<PosixAppRuntime*>(runtime_ptr);
dlerror(); // clear any pending error, per dlsym(3)'s own recommended idiom for telling a NULL
// symbol address apart from a real lookup failure
// Clear any pending error, per dlsym(3)'s own recommended idiom for telling a NULL symbol address apart from a real lookup failure
dlerror();
void* symbol = dlsym(runtime->handle, "main");
const char* lookup_error = dlerror();
if (symbol == nullptr || lookup_error != nullptr) {
+14 -1
View File
@@ -15,14 +15,27 @@ set_target_properties(app_posix_module_test_fixture PROPERTIES POSITION_INDEPEND
set(NON_ELF_FIXTURE_PATH "${CMAKE_CURRENT_BINARY_DIR}/not-elf.so")
file(WRITE "${NON_ELF_FIXTURE_PATH}" "not an elf file")
# An install-directory-shaped fixture: {dir}/bin/posix-{arch}/app.so, matching where
# app_posix_loader_service.cpp's resolve_app_path() looks for a "packaged" app's single binary.
set(INSTALL_DIR_FIXTURE_PATH "${CMAKE_CURRENT_BINARY_DIR}/install-dir-fixture")
set(INSTALL_DIR_FIXTURE_BIN_DIR "${INSTALL_DIR_FIXTURE_PATH}/bin/posix-${CMAKE_SYSTEM_PROCESSOR}")
add_custom_command(
OUTPUT "${INSTALL_DIR_FIXTURE_BIN_DIR}/app.so"
COMMAND ${CMAKE_COMMAND} -E make_directory "${INSTALL_DIR_FIXTURE_BIN_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:app_posix_module_test_fixture>" "${INSTALL_DIR_FIXTURE_BIN_DIR}/app.so"
DEPENDS app_posix_module_test_fixture
)
add_custom_target(install_dir_fixture DEPENDS "${INSTALL_DIR_FIXTURE_BIN_DIR}/app.so")
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
add_executable(AppPosixModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
add_dependencies(AppPosixModuleTests app_posix_module_test_fixture)
add_dependencies(AppPosixModuleTests app_posix_module_test_fixture install_dir_fixture)
target_include_directories(AppPosixModuleTests PRIVATE ${DOCTESTINC})
target_compile_definitions(AppPosixModuleTests PRIVATE
FIXTURE_APP_PATH="$<TARGET_FILE:app_posix_module_test_fixture>"
FIXTURE_NON_ELF_PATH="${NON_ELF_FIXTURE_PATH}"
FIXTURE_INSTALL_DIR_PATH="${INSTALL_DIR_FIXTURE_PATH}"
)
add_test(NAME AppPosixModuleTests COMMAND AppPosixModuleTests)
@@ -148,6 +148,12 @@ TEST_CASE("app_is_executable() rejects a nonexistent path") {
TEST_CASE("app_is_executable() rejects an install-directory-shaped path missing its per-arch .so") {
ensure_path_loader_registered();
// FIXTURE_DIR itself has no elf/posix-<arch>.so under it, so resolution fails.
// FIXTURE_DIR itself has no bin/posix-<arch>/app.so under it, so resolution fails.
CHECK_FALSE(is_executable_path(FIXTURE_DIR.c_str()));
}
TEST_CASE("app_is_executable() accepts an install-directory-shaped path with bin/<arch>/app.so") {
ensure_path_loader_registered();
CHECK(is_executable_path(FIXTURE_INSTALL_DIR_PATH));
}
@@ -1,9 +0,0 @@
#pragma once
#include <string>
namespace tt::app::appdetails {
void start(const std::string& appId);
} // namespace
@@ -0,0 +1,9 @@
#pragma once
#include <string>
namespace tt::app::apppackagedetails {
void start(const std::string& packageId);
} // namespace
+2 -2
View File
@@ -68,12 +68,12 @@ std::string getUserHomePath() {
}
std::string getAppInstallPath(const std::string& appId) {
assert(app_id_is_valid(appId.c_str()));
assert(app_manifest_id_is_valid(appId.c_str()));
return std::format("{}/{}", getAppInstallPath(), appId);
}
std::string getAppUserPath(const std::string& appId) {
assert(app_id_is_valid(appId.c_str()));
assert(app_manifest_id_is_valid(appId.c_str()));
return std::format("{}/app/{}", getUserHomePath(), appId);
}
+5 -5
View File
@@ -26,9 +26,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/module.h>
#include <app/start.h>
#include <Tactility/Tactility.h>
@@ -150,9 +150,9 @@ namespace app {
namespace alertdialog { extern const ::AppManifest manifest; }
namespace apphub { extern const ::AppManifest manifest; }
namespace apphubdetails { extern const ::AppManifest manifest; }
namespace appdetails { extern const ::AppManifest manifest; }
namespace apppackagedetails { extern const ::AppManifest manifest; }
namespace applist { extern const ::AppManifest manifest; }
namespace appsettings { extern const ::AppManifest manifest; }
namespace apppackagelist { extern const ::AppManifest manifest; }
namespace audiosettings { extern const ::AppManifest manifest; }
namespace boot { extern const ::AppManifest manifest; }
namespace development { extern const ::AppManifest manifest; }
@@ -212,11 +212,11 @@ static void registerInternalApps() {
LOG_I(TAG, "Registering internal apps");
app_manager_add(&app::alertdialog::manifest);
app_manager_add(&app::appdetails::manifest);
app_manager_add(&app::apppackagedetails::manifest);
app_manager_add(&app::apphub::manifest);
app_manager_add(&app::apphubdetails::manifest);
app_manager_add(&app::applist::manifest);
app_manager_add(&app::appsettings::manifest);
app_manager_add(&app::apppackagelist::manifest);
if (service::audio::isAvailable()) {
app_manager_add(&app::audiosettings::manifest);
}
@@ -2,9 +2,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -7,11 +7,11 @@
#include <app/event.h>
#include <app/install.h>
#include <app/metadata.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <http/download.h>
@@ -204,9 +204,9 @@ void updateViews(Context* ctx) {
if (is_installed) {
std::string metadata_path = std::string(install_path) + "/manifest.properties";
AppMetadata metadata;
if (app_metadata_parse(metadata_path.c_str(), &metadata) == ERROR_NONE
&& metadata.app_version_code < ctx->entry.appVersionCode) {
PackageManifest package;
if (app_package_manifest_parse(metadata_path.c_str(), &package, nullptr, 0) == ERROR_NONE
&& package.version_code < ctx->entry.appVersionCode) {
ctx->updateButton = lvgl_toolbar_add_image_button_action(ctx->toolbar, LV_SYMBOL_DOWNLOAD, onUpdatePressed, ctx);
lv_obj_remove_flag(ctx->updateLabel, LV_OBJ_FLAG_HIDDEN);
}
+1 -1
View File
@@ -1,8 +1,8 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -1,9 +1,10 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/install.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <format>
@@ -12,17 +13,15 @@
#include <lvgl_window_manager/window_manager.h>
#include <Tactility/StringUtils.h>
#include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/file/File.h>
#include <Tactility/lvgl/Style.h>
#include <tactility/check.h>
#include <tactility/log.h>
constexpr auto* TAG = "AppDetails";
constexpr auto* TAG = "AppPackageDetails";
namespace tt::app::appdetails {
namespace tt::app::apppackagedetails {
extern const ::AppManifest manifest;
@@ -30,13 +29,34 @@ namespace {
struct Context {
uint32_t appInstanceId;
std::string targetAppId;
// findAppManifestById() returns the old-model registry's AppManifest type - AppDetails
// shows details for apps in that registry regardless of which system they run under.
AppManifest targetManifest = { };
std::string targetPackageId;
PackageManifest targetPackage = {};
std::vector<std::string> appIds;
uint32_t pendingUninstallDialogId = 0;
};
struct FindPackageContext {
const std::string* packageId;
PackageManifest* outPackage;
std::vector<std::string>* outAppIds;
bool found = false;
};
void onVisitPackage(const ::AppPackage* pkg, void* context) {
auto* findContext = static_cast<FindPackageContext*>(context);
if (findContext->found || *findContext->packageId != pkg->package.id) {
return;
}
*findContext->outPackage = pkg->package;
findContext->outAppIds->assign(pkg->app_ids, pkg->app_ids + pkg->app_id_count);
findContext->found = true;
}
bool findPackage(const std::string& packageId, PackageManifest& outPackage, std::vector<std::string>& outAppIds) {
FindPackageContext findContext { &packageId, &outPackage, &outAppIds };
app_manager_for_each_package(onVisitPackage, &findContext);
return findContext.found;
}
void onPressUninstall(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
@@ -44,7 +64,7 @@ void onPressUninstall(lv_event_t* event) {
ctx->pendingUninstallDialogId = alertdialog::start(
ctx->appInstanceId,
"Confirmation",
std::format("Uninstall {}?", ctx->targetManifest.name),
std::format("Uninstall {}?", ctx->targetPackage.id),
choices
);
}
@@ -59,7 +79,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto title = std::format("{} details", ctx->targetManifest.name);
auto title = std::format("{} details", ctx->targetPackage.id);
auto* toolbar = lvgl_toolbar_create(parent, title.c_str());
// The global toolbar nav callback only knows how to stop old-model apps.
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
@@ -71,35 +91,39 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
lvgl::obj_set_style_bg_invisible(wrapper);
auto identifier = std::format("Identifier: {}", ctx->targetManifest.id);
auto identifier = std::format("Identifier: {}", ctx->targetPackage.id);
auto* identifier_label = lv_label_create(wrapper);
lv_label_set_text(identifier_label, identifier.c_str());
auto* location_label = lv_label_create(wrapper);
std::string location;
bool is_internal = ctx->targetManifest.location.type == APP_LOCATION_MEMORY;
bool is_external = ctx->targetManifest.location.type == APP_LOCATION_PATH;
if (is_internal) {
location = "internal";
} else if (is_external) {
if (!string::getPathParent(static_cast<const char*>(ctx->targetManifest.location.location), location)) {
location = "external";
}
} else {
LOG_E(TAG, "Unknown app location type %d", ctx->targetManifest.location.type);
return;
}
std::string location_label_text = std::format("Location: {}", location);
lv_label_set_text(location_label, location_label_text.c_str());
auto version = std::format("Version: {} ({})", ctx->targetPackage.version_name, ctx->targetPackage.version_code);
auto* version_label = lv_label_create(wrapper);
lv_label_set_text(version_label, version.c_str());
if (is_external) {
auto* uninstall_button = lv_button_create(wrapper);
lv_obj_set_width(uninstall_button, LV_PCT(100));
lv_obj_add_event_cb(uninstall_button, onPressUninstall, LV_EVENT_SHORT_CLICKED, ctx);
auto* uninstall_label = lv_label_create(uninstall_button);
lv_obj_align(uninstall_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(uninstall_label, "Uninstall");
char install_path[192];
std::string location = "unknown";
if (app_get_install_path(ctx->targetPackage.id, install_path, sizeof(install_path)) == ERROR_NONE) {
location = install_path;
}
auto location_text = std::format("Location: {}", location);
auto* location_label = lv_label_create(wrapper);
lv_label_set_text(location_label, location_text.c_str());
std::string apps;
for (const auto& appId : ctx->appIds) {
AppManifest appManifest {};
const char* label = app_manager_find_manifest(appId.c_str(), &appManifest) == ERROR_NONE ? appManifest.name : appId.c_str();
apps += apps.empty() ? label : std::format(", {}", label);
}
auto apps_text = std::format("Apps: {}", apps);
auto* apps_label = lv_label_create(wrapper);
lv_label_set_text(apps_label, apps_text.c_str());
auto* uninstall_button = lv_button_create(wrapper);
lv_obj_set_width(uninstall_button, LV_PCT(100));
lv_obj_add_event_cb(uninstall_button, onPressUninstall, LV_EVENT_SHORT_CLICKED, ctx);
auto* uninstall_label = lv_label_create(uninstall_button);
lv_obj_align(uninstall_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(uninstall_label, "Uninstall");
}
int32_t appMain(int argc, char* argv[]) {
@@ -107,9 +131,9 @@ int32_t appMain(int argc, char* argv[]) {
Context ctx {};
ctx.appInstanceId = appInstanceId;
ctx.targetAppId = (argc > 0) ? argv[0] : std::string();
if (app_manager_find_manifest(ctx.targetAppId.c_str(), &ctx.targetManifest) != ERROR_NONE) {
LOG_W(TAG, "App %s not found", ctx.targetAppId.c_str());
ctx.targetPackageId = (argc > 0) ? argv[0] : std::string();
if (!findPackage(ctx.targetPackageId, ctx.targetPackage, ctx.appIds)) {
LOG_W(TAG, "Package %s not found", ctx.targetPackageId.c_str());
return 0;
}
@@ -134,7 +158,7 @@ int32_t appMain(int argc, char* argv[]) {
case APP_EVENT_RESULT:
if (event.result.launch_id == ctx.pendingUninstallDialogId) {
if (event.result.result == 0) { // 0 = Yes
app_uninstall(ctx.targetManifest.id);
app_uninstall(ctx.targetPackage.id);
shouldClose = true;
}
app_manager_stop(event.result.launch_id);
@@ -156,15 +180,15 @@ int32_t appMain(int argc, char* argv[]) {
} // namespace
void start(const std::string& appId) {
const char* argv[] = { appId.c_str() };
void start(const std::string& packageId) {
const char* argv[] = { packageId.c_str() };
uint32_t instanceId = 0;
app_start(manifest.id, 1, argv, &instanceId);
}
extern const ::AppManifest manifest = {
.id = "tactility.appdetails",
.name = "App Details",
.id = "tactility.apppackagedetails",
.name = "Package Details",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN,
@@ -1,11 +1,12 @@
#include <lvgl/icons/shared.h>
#include <lvgl/fonts.h>
#include <Tactility/app/appdetails/AppDetails.h>
#include <Tactility/app/apppackagedetails/AppPackageDetails.h>
#include <app/event.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/scheduler.h>
#include <lvgl_window_manager/window_manager.h>
@@ -18,7 +19,7 @@
#include <cstring>
#include <vector>
namespace tt::app::appsettings {
namespace tt::app::apppackagelist {
extern const ::AppManifest manifest;
@@ -26,11 +27,13 @@ namespace {
struct Context {
uint32_t appInstanceId;
// Must outlive the widgets - button user-data points into this, not a createWidgets()-local vector.
std::vector<std::string> packageIds;
};
void onAppPressed(lv_event_t* e) {
const auto* target_manifest = static_cast<const ::AppManifest*>(lv_event_get_user_data(e));
appdetails::start(target_manifest->id);
void onPackagePressed(lv_event_t* e) {
auto* packageId = static_cast<char*>(lv_event_get_user_data(e));
apppackagedetails::start(packageId);
}
void onBackPressed(lv_event_t* event) {
@@ -38,18 +41,16 @@ void onBackPressed(lv_event_t* event) {
app_event_emit_close(ctx->appInstanceId);
}
void createAppWidget(const ::AppManifest* target_manifest, lv_obj_t* list) {
// The new AppManifest has no per-app icon - use a shared generic one for every entry, same
// fallback AppList.cpp uses.
lv_obj_t* btn = lv_list_add_button(list, LVGL_ICON_SHARED_TOOLBAR, target_manifest->name);
void createPackageWidget(const char* packageId, lv_obj_t* list) {
lv_obj_t* btn = lv_list_add_button(list, LVGL_ICON_SHARED_TOOLBAR, packageId);
lv_obj_t* image = lv_obj_get_child(btn, 0);
lv_obj_set_style_text_font(image, lvgl_get_shared_icon_font(), LV_PART_MAIN);
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, const_cast<::AppManifest*>(target_manifest));
lv_obj_add_event_cb(btn, &onPackagePressed, LV_EVENT_SHORT_CLICKED, const_cast<char*>(packageId));
}
void collectManifest(const ::AppManifest* manifest, void* context) {
auto* manifests = static_cast<std::vector<const ::AppManifest*>*>(context);
manifests->push_back(manifest);
void collectPackageId(const ::AppPackage* pkg, void* context) {
auto* packageIds = static_cast<std::vector<std::string>*>(context);
packageIds->emplace_back(pkg->package.id);
}
void createWidgets(lv_obj_t* parent, void* userData) {
@@ -68,21 +69,16 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1);
std::vector<const ::AppManifest*> manifests;
app_manager_for_each_manifest(collectManifest, &manifests);
std::ranges::sort(manifests, [](const ::AppManifest* a, const ::AppManifest* b) {
return strcmp(a->name, b->name) < 0;
});
// createWidgets() can rerun for this same Context (window rebuild-on-remove).
ctx->packageIds.clear();
app_manager_for_each_package(collectPackageId, &ctx->packageIds);
std::ranges::sort(ctx->packageIds);
size_t app_count = 0;
for (const auto* target_manifest: manifests) {
if (target_manifest->location.type == APP_LOCATION_PATH) {
app_count++;
createAppWidget(target_manifest, list);
}
for (const auto& packageId : ctx->packageIds) {
createPackageWidget(packageId.c_str(), list);
}
if (app_count == 0) {
if (ctx->packageIds.empty()) {
// lv_obj_align() is ignored for children of a flex-managed parent, so the empty-state
// label needs its own flex-growing wrapper to center within; the (empty) list is hidden
// rather than deleted so the wrapper can just take its place in the flex flow.
@@ -139,7 +135,7 @@ int32_t appMain(int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "tactility.appsettings",
.id = "tactility.apppackagelist",
.name = "Apps",
.category = APP_CATEGORY_SETTINGS,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
+1 -1
View File
@@ -10,9 +10,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
+1 -1
View File
@@ -5,9 +5,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -10,9 +10,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -11,8 +11,8 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/start.h>
#include <app/scheduler.h>
#include <lvgl_window_manager/window_manager.h>
@@ -7,9 +7,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -6,9 +6,9 @@
#include <app/event.h>
#include <app/io.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <app/stream.h>
#include <lvgl_window_manager/window_manager.h>
@@ -8,9 +8,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -7,10 +7,10 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/paths.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -6,9 +6,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -2,9 +2,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <app/stream.h>
#include <lvgl_window_manager/window_manager.h>
+1 -1
View File
@@ -1,8 +1,8 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <cstring>
+1 -1
View File
@@ -7,9 +7,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <app/stream.h>
#include <lvgl_window_manager/window_manager.h>
@@ -2,9 +2,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
+1 -1
View File
@@ -1,8 +1,8 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
+1 -1
View File
@@ -10,9 +10,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -5,9 +5,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
+1 -1
View File
@@ -8,9 +8,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -5,9 +5,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -6,9 +6,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -6,9 +6,9 @@
#include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h>
@@ -190,7 +190,7 @@ error_t DevelopmentService::handleAppInstall(HttpServerRequest* request, void*)
if (
name_entry == content_disposition_map.end() ||
filename_entry == content_disposition_map.end() ||
name_entry->second != "elf"
name_entry->second != "app"
) {
http_server_request_send_error(request, 400, "Multipart form error: name or filename parameter missing or mismatching");
return ERROR_UNDEFINED;
+8 -6
View File
@@ -1,7 +1,9 @@
manifest.version=0.2
target.sdk=0.0.0
manifest.version=0.3
target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4,posix-x86_64
app.id=tactility.sdktest
app.version.name=0.1.0
app.version.code=1
app.name=SDK Test
id=tactility.sdktest
version.name=0.1.0
version.code=1
app.0.id=sdktest
app.0.name=SDK Test
app.0.binary=main
+350 -125
View File
@@ -12,7 +12,7 @@ import tarfile
from urllib.parse import urlparse
ttbuild_path = ".tactility"
ttbuild_version = "5.0.1"
ttbuild_version = "6.0.0"
ttbuild_cdn = "https://cdn.tactilityproject.org"
ttbuild_sdk_json_validity = 3600 # seconds
ttport = 6666
@@ -21,7 +21,7 @@ use_local_sdk = False
local_base_path = None
http_timeout_seconds = 10
# App install uploads the whole package over HTTP and the device only responds once it's
# fully received, extracted and registered - large packages (e.g. bundled fonts/assets) can
# fully received, extracted and registered; large packages (e.g. bundled fonts/assets) can
# easily take well over http_timeout_seconds on a slow SD card, so give it a lot more room.
install_timeout_seconds = 120
@@ -33,30 +33,55 @@ shell_color_cyan = "\033[36m"
shell_color_reset = "\033[m"
def print_help():
print("Usage: python tactility.py [app_path] [action] [options]")
print("Usage: python tactility.py [action] [options]")
print("")
print("Actions:")
print(" build [platform] Build the app. Optionally specify a platform.")
print("")
print(" build Build the app. Optionally specify a platform.")
print(" Supported platforms are lower case. Example: esp32s3")
print(" Supported platforms are read from manifest.properties")
print(" Parameters:")
print(" (optional) --architecture [input], -a [input]")
print("")
print(" clean Clean the build folders")
print("")
print(" clearcache Clear the SDK cache")
print("")
print(" updateself Update this tool")
print(" run [ip] Run the application")
print(" install [ip] Install the application")
print(" uninstall [ip] Uninstall the application")
print(" bir [ip] [platform] Build, install then run. Optionally specify a platform.")
print(" brrr [ip] [platform] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.")
print("")
print(" run Run the application")
print(" Parameters:")
print(" (required) --host, -h [input]")
print("")
print(" install Install the application")
print(" Parameters:")
print(" (required) --host, -h [input]")
print("")
print(" uninstall Uninstall the application")
print(" Parameters:")
print(" (required) --host, -h [input]")
print("")
print(" bir Build, install then run.")
print(" Parameters:")
print(" (required) --host, -h [input]")
print(" (optional) --architecture, -a [input]")
print("")
print(" brrr Functionally the same as \"bir\", but \"app goes brrr\" meme variant.")
print(" Parameters:")
print(" (required) --host, -h [input]")
print(" (optional) --architecture, -a [input]")
print("")
print("Options:")
print(" -p, --path [input] Path to the app directory (defaults to current directory)")
print(" --help Show this commandline info")
print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH with platform subfolders matching target platforms.")
print(" --skip-build Run everything except the idf.py/CMake commands")
print(" --verbose Show extra console output")
print("")
print("Examples:")
print(" python tactility.py Apps/Snake build esp32s3 --verbose")
print(" python tactility.py Apps/Snake bir 192.168.1.50 esp32s3")
print(" python tactility.py build")
print(" python tactility.py build --path Apps/Snake --architecture esp32s3 --verbose")
print(" python tactility.py bir --path Apps/Snake --host 192.168.1.50 --architecture esp32s3")
# region Core
@@ -105,8 +130,8 @@ def exit_with_error(message):
print_error(message)
sys.exit(1)
def get_url(ip, path):
return f"http://{ip}:{ttport}{path}"
def get_url(host, path):
return f"http://{host}:{ttport}{path}"
def read_properties_file(path):
properties = {}
@@ -123,6 +148,126 @@ def read_properties_file(path):
#endregion Core
#region Versioning
class SemanticVersion:
def __init__(self, major, minor, patch, tag=None):
self.major = major
self.minor = minor
self.patch = patch
self.tag = tag
@staticmethod
def parse(version_string):
match = re.match(r"^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$", version_string)
if match is None:
exit_with_error(f"Invalid version format: {version_string}")
major, minor, patch, tag = match.groups()
return SemanticVersion(int(major), int(minor), int(patch), tag)
def _numeric(self):
return (self.major, self.minor, self.patch)
def __eq__(self, other):
return self._numeric() == other._numeric() and self.tag == other.tag
def __lt__(self, other):
if self._numeric() != other._numeric():
return self._numeric() < other._numeric()
# Same major.minor.patch: an untagged version outranks any tagged (pre-release) one.
if self.tag == other.tag:
return False
if self.tag is None:
return False
if other.tag is None:
return True
return self.tag < other.tag
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}" + (f"-{self.tag}" if self.tag else "")
# Ignores tags: a "-dev"/"-rc1"-tagged pre-release build of a version is still that version,
# not an older one, unlike __lt__'s full ordering (which ranks pre-releases below releases).
def is_older_release_of(self, other):
return self._numeric() < other._numeric()
# Append a new entry whenever the tool drops support for older SDKs.
# The last entry is the currently effective minimum.
SDK_COMPATIBILITY_LEDGER = [
SemanticVersion.parse("0.8.0"),
]
def minimum_supported_sdk_version():
return SDK_COMPATIBILITY_LEDGER[-1]
def validate_sdk_compatibility(target_sdk_version):
minimum = minimum_supported_sdk_version()
if SemanticVersion.parse(target_sdk_version).is_older_release_of(minimum):
exit_with_error(
f"This tool requires SDK version {minimum} or newer "
f"(manifest.properties requests {target_sdk_version})"
)
#endregion Versioning
#region Argument parsing
# canonical name -> (short flag or None, long flag, takes_value)
ARG_SPECS = {
"path": ("-p", "--path", True),
"host": ("-h", "--host", True),
"platform": ("-a", "--architecture", True),
"verbose": (None, "--verbose", False),
"skip-build": (None, "--skip-build", False),
"local-sdk": (None, "--local-sdk", False),
"help": (None, "--help", False),
}
class Arguments:
def __init__(self, values):
self.values = values
def get(self, key, default=None):
return self.values.get(key, default)
def has(self, key):
return key in self.values
class ParsedCommand:
def __init__(self, action, args):
self.action = action
self.arguments = args
def parse_command_line(argv):
token_to_spec = {}
for canonical, (short, long, takes_value) in ARG_SPECS.items():
if short is not None:
token_to_spec[short] = (canonical, takes_value)
token_to_spec[long] = (canonical, takes_value)
action = None
args = {}
index = 0
while index < len(argv):
token = argv[index]
if token in token_to_spec:
canonical, takes_value = token_to_spec[token]
if takes_value:
index += 1
if index >= len(argv):
exit_with_error(f"Missing value for {token}")
args[canonical] = argv[index]
else:
args[canonical] = "true"
elif action is None:
action = token
else:
exit_with_error(f"Unexpected argument: {token}")
index += 1
return ParsedCommand(action, Arguments(args))
#endregion Argument parsing
#region SDK helpers
def read_sdk_json():
@@ -232,7 +377,7 @@ def validate_self(sdk_json):
exit_with_error("Server returned invalid SDK data format (toolDownloadUrl not found)")
tool_version = sdk_json["toolVersion"]
tool_compatibility = sdk_json["toolCompatibility"]
if tool_version != ttbuild_version:
if SemanticVersion.parse(ttbuild_version) < SemanticVersion.parse(tool_version):
print_warning(f"New version available: {tool_version} (currently using {ttbuild_version})")
print_warning(f"Run 'tactility.py updateself' to update.")
if re.search(tool_compatibility, ttbuild_version) is None:
@@ -247,10 +392,70 @@ def validate_self(sdk_json):
def read_manifest():
return read_properties_file("manifest.properties")
def is_v3_manifest(manifest):
return manifest.get("manifest.version") == "0.3"
# The package identifier: a v2 manifest has no package/app distinction, so its single "app.id"
# doubles as both; a v3 manifest's package id is the bare "id" key, distinct from any of its
# (possibly several) "app.N.id" app ids.
def get_package_id(manifest):
return manifest["id"] if is_v3_manifest(manifest) else manifest["app.id"]
# The id of the app "run"/"install" operate on. A v2 manifest has exactly one; a v3 manifest may
# declare several, so this picks the first ("app.0.id") as the one a single-app dev workflow means.
def get_primary_app_id(manifest):
return manifest["app.0.id"] if is_v3_manifest(manifest) else manifest["app.id"]
def validate_manifest(manifest):
for key in ("manifest.version", "target.sdk", "target.platforms", "app.id", "app.version.name", "app.version.code", "app.name"):
for key in ("manifest.version", "target.sdk", "target.platforms"):
if key not in manifest:
exit_with_error(f"Invalid manifest format: {key} not found")
if manifest["manifest.version"] not in ("0.2", "0.3"):
exit_with_error(f"Unsupported manifest.version: {manifest['manifest.version']}")
if is_v3_manifest(manifest):
for key in ("id", "version.name", "version.code"):
if key not in manifest:
exit_with_error(f"Invalid manifest format: {key} not found")
if "app.0.id" not in manifest:
exit_with_error("Invalid manifest format: app.0.id not found")
index = 0
while f"app.{index}.id" in manifest:
for suffix in ("name", "binary"):
key = f"app.{index}.{suffix}"
if key not in manifest:
exit_with_error(f"Invalid manifest format: {key} not found")
index += 1
else:
for key in ("app.id", "app.version.name", "app.version.code", "app.name"):
if key not in manifest:
exit_with_error(f"Invalid manifest format: {key} not found")
validate_sdk_compatibility(manifest["target.sdk"])
# Maps each binary this app builds to the directory its own CMake project lives in. A v2
# manifest always describes exactly one app, built at the app's own root (see
# package_intermediate_binaries() for its fixed "app.{elf,so}" package filename, matching
# package_manifest_parse_v2()'s single implicit binary). A v3 manifest declaring a single
# "app.0.*" block is the same case: built at the app's own root. Only when a v3 manifest
# declares more than one "app.N.*" block (0-indexed, contiguous - the first missing "app.N.id"
# ends the list, mirroring package_manifest_parse_v3()) does each get its own subdirectory,
# named after its own "app.N.binary" (the filename it installs as - see
# package_manifest_parse_v3()'s doc).
def get_binary_dirs(manifest):
if is_v3_manifest(manifest):
binaries = []
index = 0
while f"app.{index}.id" in manifest:
binary = manifest[f"app.{index}.binary"]
binaries.append((binary, binary))
index += 1
if len(binaries) == 1:
return [(binaries[0][0], ".")]
for binary, directory in binaries:
if not os.path.isdir(directory):
exit_with_error(f"Binary directory not found for '{binary}': {directory}")
return binaries
else:
return [(manifest["app.id"], ".")]
def is_valid_manifest_platform(manifest, platform):
manifest_platforms = manifest["target.platforms"].split(",")
@@ -345,7 +550,7 @@ tactility_project_post(%(app_id)s)
def cmakelists_version_marker():
return f"# tactility-cmakelists-version: {CMAKELISTS_VERSION}"
def ensure_cmakelists_up_to_date(manifest):
def ensure_cmakelists_up_to_date(project_id):
marker = cmakelists_version_marker()
if os.path.exists("CMakeLists.txt"):
with open("CMakeLists.txt", "r") as file:
@@ -353,7 +558,7 @@ def ensure_cmakelists_up_to_date(manifest):
if first_line == marker:
return
print(f"Updating CMakeLists.txt to {marker}")
content = CMAKELISTS_TEMPLATE % {"version": CMAKELISTS_VERSION, "app_id": manifest["app.id"]}
content = CMAKELISTS_TEMPLATE % {"version": CMAKELISTS_VERSION, "app_id": project_id}
with open("CMakeLists.txt", "w") as file:
file.write(content)
@@ -532,43 +737,58 @@ def package_intermediate_manifest(target_path):
shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties"))
return True
def package_intermediate_binaries(target_path, platforms):
elf_dir = os.path.join(target_path, "elf")
os.makedirs(elf_dir, exist_ok=True)
for platform in platforms:
elf_path = find_elf_file(platform)
if elf_path is None:
print_error(f"ELF file not found for {platform}")
return False
# app-posix-module's loader resolves an installed app to "elf/posix-<arch>.so", matching
# its own compile-time architecture, not "*.elf".
extension = ".so" if platform.startswith("posix") else ".elf"
shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}{extension}"))
def get_artifact_extension(platform):
# POSIX apps are dlopen()ed shared objects (app-posix-module), not idf.py/elf_loader
# relocatable images, so they land as a plain ".so" instead of ".elf".
return ".so" if platform.startswith("posix") else ".elf"
def package_intermediate_binaries(target_path, platforms, manifest):
# Each binary is built in its own directory (get_binary_dirs()), with its own
# build/cmake-build-{platform} tree; chdir into it so find_elf_file()/get_cmake_path()
# (both CWD-relative) resolve against the right one, then restore CWD for the next binary.
original_cwd = os.getcwd()
for binary, directory in get_binary_dirs(manifest):
os.chdir(directory)
try:
for platform in platforms:
elf_path = find_elf_file(platform)
if elf_path is None:
print_error(f"ELF file not found for '{binary}' on {platform}")
return False
# v2's single app always installs at the fixed path bin/{platform}/app.{elf,so}
# (package_manifest_parse_v2()); v3 apps install under their own declared
# app.N.binary name (package_manifest_parse_v3()).
artifact_name = binary if is_v3_manifest(manifest) else "app"
platform_dir = os.path.join(target_path, "bin", platform)
os.makedirs(platform_dir, exist_ok=True)
shutil.copy(elf_path, os.path.join(platform_dir, f"{artifact_name}{get_artifact_extension(platform)}"))
finally:
os.chdir(original_cwd)
return True
def package_intermediate_assets(target_path):
if os.path.isdir("assets"):
shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True)
def package_intermediate(platforms):
target_path = os.path.join("build", "package-intermediate")
def package_intermediate(platforms, manifest):
target_path = os.path.abspath(os.path.join("build", "package-intermediate"))
if os.path.isdir(target_path):
shutil.rmtree(target_path)
os.makedirs(target_path, exist_ok=True)
if not package_intermediate_manifest(target_path):
return False
if not package_intermediate_binaries(target_path, platforms):
if not package_intermediate_binaries(target_path, platforms, manifest):
return False
package_intermediate_assets(target_path)
return True
def package_name(manifest):
return os.path.join("build", f"{manifest['app.id']}.app")
return os.path.join("build", f"{get_package_id(manifest)}.app")
def package_all(manifest, platforms):
status = f"Building package with {platforms}"
print_status_busy(status)
if not package_intermediate(platforms):
if not package_intermediate(platforms, manifest):
print_status_error("Building package failed: missing inputs")
return False
# Create build/something.app
@@ -588,8 +808,9 @@ def setup_environment():
global ttbuild_path
os.makedirs(ttbuild_path, exist_ok=True)
def build_action(manifest, platform_arg, skip_build):
ensure_cmakelists_up_to_date(manifest)
def build_action(manifest, arguments):
platform_arg = arguments.get("platform")
skip_build = arguments.has("skip-build")
platforms_to_build = get_manifest_target_platforms(manifest, platform_arg)
# Environment validation
validate_environment(platforms_to_build)
@@ -598,10 +819,10 @@ def build_action(manifest, platform_arg, skip_build):
global local_base_path
local_base_path = os.environ.get("TACTILITY_SDK_PATH")
validate_local_sdks(platforms_to_build, manifest["target.sdk"])
if should_fetch_sdkconfig_files(platforms_to_build):
fetch_sdkconfig_files(platforms_to_build)
if not use_local_sdk:
sdk_json = read_sdk_json()
validate_self(sdk_json)
@@ -610,19 +831,49 @@ def build_action(manifest, platform_arg, skip_build):
if not use_local_sdk:
if not sdk_download_all(sdk_version, platforms_to_build):
exit_with_error("Failed to download one or more SDKs")
if not build_all(sdk_version, platforms_to_build, skip_build): # Environment validation
return False
# A multi-binary app builds each of its binaries in its own subdirectory as an independent
# CMake project; a single-binary app has exactly one binary, built at the app root
# ("." from get_binary_dirs()), matching the tool's original single-binary behavior exactly.
original_cwd = os.getcwd()
for binary, directory in get_binary_dirs(manifest):
os.chdir(directory)
try:
ensure_cmakelists_up_to_date(binary if directory == "." else f"{get_package_id(manifest)}.{binary}")
if not build_all(sdk_version, platforms_to_build, skip_build):
return False
finally:
os.chdir(original_cwd)
if not skip_build:
if not package_all(manifest, platforms_to_build):
return False
return True
def clean_action():
def clean_action(manifest):
cleaned_any = False
# The app root always has its own build/ (package-intermediate + the final .app/.elf,
# regardless of binary count). A multi-binary app additionally has one build/ per binary
# subdirectory (get_binary_dirs()), which "." (the root, handled above) doesn't repeat.
if os.path.exists("build"):
print_status_busy("Removing build/")
shutil.rmtree("build")
print_status_success("Removed build/")
else:
cleaned_any = True
original_cwd = os.getcwd()
for _, directory in get_binary_dirs(manifest):
if directory == ".":
continue
os.chdir(directory)
try:
if os.path.exists("build"):
print_status_busy(f"Removing {directory}/build/")
shutil.rmtree("build")
print_status_success(f"Removed {directory}/build/")
cleaned_any = True
finally:
os.chdir(original_cwd)
if not cleaned_any:
print("Nothing to clean")
def clear_cache_action():
@@ -641,9 +892,9 @@ def update_self_action():
else:
exit_with_error("Update failed")
def get_device_info(ip):
def get_device_info(host):
print_status_busy(f"Requesting device info")
url = get_url(ip, "/info")
url = get_url(host, "/info")
try:
response = requests.get(url, timeout=http_timeout_seconds)
if response.status_code != 200:
@@ -654,10 +905,13 @@ def get_device_info(ip):
except requests.RequestException as e:
print_status_error(f"Device info request failed: {e}")
def run_action(manifest, ip):
app_id = manifest["app.id"]
def run_action(manifest, arguments):
host = arguments.get("host")
if host is None:
exit_with_error("Missing required argument: --host")
app_id = get_primary_app_id(manifest)
print_status_busy("Running")
url = get_url(ip, "/app/run")
url = get_url(host, "/app/run")
params = {'id': app_id}
try:
response = requests.post(url, params=params, timeout=http_timeout_seconds)
@@ -668,21 +922,21 @@ def run_action(manifest, ip):
except requests.RequestException as e:
print_status_error(f"Running request failed: {e}")
def install_action(manifest, ip, platforms):
print_status_busy("Installing")
for platform in platforms:
elf_path = find_elf_file(platform)
if elf_path is None:
print_status_error(f"ELF file not built for {platform}")
return False
def install_action(manifest, arguments):
host = arguments.get("host")
if host is None:
exit_with_error("Missing required argument: --host")
package_path = package_name(manifest)
# print(f"Installing {package_path} to {ip}")
url = get_url(ip, "/app/install")
if not os.path.isfile(package_path):
print_status_error(f"Package not found: {package_path} (run 'build' first)")
return False
print_status_busy("Installing")
url = get_url(host, "/app/install")
try:
# Prepare multipart form data
with open(package_path, 'rb') as file:
files = {
'elf': file
'app': file
}
response = requests.put(url, files=files, timeout=install_timeout_seconds)
if response.status_code != 200:
@@ -698,10 +952,13 @@ def install_action(manifest, ip, platforms):
print_status_error(f"Install file error: {e}")
return False
def uninstall_action(manifest, ip):
app_id = manifest["app.id"]
def uninstall_action(manifest, arguments):
host = arguments.get("host")
if host is None:
exit_with_error("Missing required argument: --host")
app_id = get_package_id(manifest)
print_status_busy("Uninstalling")
url = get_url(ip, "/app/uninstall")
url = get_url(host, "/app/uninstall")
params = {'id': app_id}
try:
response = requests.put(url, params=params, timeout=http_timeout_seconds)
@@ -716,38 +973,34 @@ def uninstall_action(manifest, ip):
if __name__ == "__main__":
print(f"Tactility Build System v{ttbuild_version}")
if "--help" in sys.argv:
# Anchor the cache to the invocation directory, before --path (below) can chdir into the app.
ttbuild_path = os.path.abspath(ttbuild_path)
argv = sys.argv[1:]
if len(argv) == 0:
print_help()
sys.exit(1)
parsed_command = parse_command_line(argv)
if parsed_command.arguments.has("help"):
print_help()
sys.exit()
# Argument validation
if len(sys.argv) == 1:
if parsed_command.action is None:
print_help()
sys.exit(1)
if "--verbose" in sys.argv:
verbose = True
sys.argv.remove("--verbose")
skip_build = False
if "--skip-build" in sys.argv:
skip_build = True
sys.argv.remove("--skip-build")
if "--local-sdk" in sys.argv:
use_local_sdk = True
sys.argv.remove("--local-sdk")
# Check if the first argument is a path to an app directory
if len(sys.argv) > 2:
potential_app_dir = sys.argv[1]
if os.path.isdir(potential_app_dir) and os.path.isfile(os.path.join(potential_app_dir, "manifest.properties")):
if verbose:
print_status_success(f"Switching to app directory: {potential_app_dir}")
os.chdir(potential_app_dir)
sys.argv = [sys.argv[0]] + sys.argv[2:]
if len(sys.argv) < 2:
print_help()
sys.exit(1)
action_arg = sys.argv[1]
verbose = parsed_command.arguments.has("verbose")
use_local_sdk = parsed_command.arguments.has("local-sdk")
app_path = parsed_command.arguments.get("path")
if app_path is not None:
if not os.path.isdir(app_path) or not os.path.isfile(os.path.join(app_path, "manifest.properties")):
exit_with_error(f"App path not found or missing manifest.properties: {app_path}")
if verbose:
print_status_success(f"Switching to app directory: {app_path}")
os.chdir(app_path)
# Environment setup
setup_environment()
@@ -755,58 +1008,30 @@ if __name__ == "__main__":
exit_with_error("manifest.properties not found")
manifest = read_manifest()
validate_manifest(manifest)
all_platform_targets = manifest["target.platforms"].split(",")
# Update SDK cache (tool.json)
if not use_local_sdk and should_update_tool_json() and not update_tool_json():
exit_with_error("Failed to retrieve SDK info")
# Actions
action_arg = parsed_command.action
if action_arg == "build":
if len(sys.argv) < 2:
print_help()
exit_with_error("Commandline parameter missing")
platform = None
if len(sys.argv) > 2:
platform = sys.argv[2]
if not build_action(manifest, platform, skip_build):
if not build_action(manifest, parsed_command.arguments):
sys.exit(1)
elif action_arg == "clean":
clean_action()
clean_action(manifest)
elif action_arg == "clearcache":
clear_cache_action()
elif action_arg == "updateself":
update_self_action()
elif action_arg == "run":
if len(sys.argv) < 3:
print_help()
exit_with_error("Commandline parameter missing")
run_action(manifest, sys.argv[2])
run_action(manifest, parsed_command.arguments)
elif action_arg == "install":
if len(sys.argv) < 3:
print_help()
exit_with_error("Commandline parameter missing")
platform = None
platforms_to_install = all_platform_targets
if len(sys.argv) >= 4:
platform = sys.argv[3]
platforms_to_install = [platform]
install_action(manifest, sys.argv[2], platforms_to_install)
install_action(manifest, parsed_command.arguments)
elif action_arg == "uninstall":
if len(sys.argv) < 3:
print_help()
exit_with_error("Commandline parameter missing")
uninstall_action(manifest, sys.argv[2])
uninstall_action(manifest, parsed_command.arguments)
elif action_arg == "bir" or action_arg == "brrr":
if len(sys.argv) < 3:
print_help()
exit_with_error("Commandline parameter missing")
platform = None
platforms_to_install = all_platform_targets
if len(sys.argv) >= 4:
platform = sys.argv[3]
platforms_to_install = [platform]
if build_action(manifest, platform, skip_build):
if install_action(manifest, sys.argv[2], platforms_to_install):
run_action(manifest, sys.argv[2])
if build_action(manifest, parsed_command.arguments):
if install_action(manifest, parsed_command.arguments):
run_action(manifest, parsed_command.arguments)
else:
print_help()
exit_with_error("Unknown commandline parameter")