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 run: python Buildscripts/release-sdk-posix.py release/TactilitySDK
- name: 'Test Integration Prep' - name: 'Test Integration Prep'
shell: bash 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: env:
TACTILITY_ARCH: ${{ steps.arch.outputs.value }} TACTILITY_ARCH: ${{ steps.arch.outputs.value }}
run: | 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 mkdir -p test_sdk/$TACTILITY_SDK_NAME
cp -r release/TactilitySDK test_sdk/$TACTILITY_SDK_NAME cp -r release/TactilitySDK test_sdk/$TACTILITY_SDK_NAME
- name: 'Test Integration' - name: 'Test Integration'
shell: bash shell: bash
env: env:
TACTILITY_ARCH: ${{ steps.arch.outputs.value }} 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' - name: 'Upload Artifact'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: 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 # 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: # 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: | run: |
TACTILITY_SDK_NAME="0.0.0-${{ inputs.arch }}" TACTILITY_SDK_NAME="$(cat version.txt)-${{ inputs.arch }}"
mkdir -p test_sdk/$TACTILITY_SDK_NAME mkdir -p test_sdk/$TACTILITY_SDK_NAME
cp -r release/TactilitySDK test_sdk/$TACTILITY_SDK_NAME cp -r release/TactilitySDK test_sdk/$TACTILITY_SDK_NAME
- name: 'Test Integration' - name: 'Test Integration'
@@ -43,7 +43,7 @@ runs:
with: with:
esp_idf_version: v5.5.2 esp_idf_version: v5.5.2
target: ${{ inputs.arch }} 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' - name: 'Upload Artifact'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@@ -16,6 +16,8 @@
#include <esp_elf.h> #include <esp_elf.h>
#include <esp_err.h> #include <esp_err.h>
#include <sys/stat.h>
#include <cstdio> #include <cstdio>
#include <new> #include <new>
#include <string> #include <string>
@@ -64,13 +66,22 @@ error_t read_file(const char* path, uint8_t** out_data, size_t* out_size) {
return ERROR_NONE; return ERROR_NONE;
} }
// location.location can be either an app's install directory or the .elf file directly; the bool is_regular_file(const std::string& path) {
// former resolves to the per-target binary at {dir}/elf/{CONFIG_IDF_TARGET}.elf. 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) { std::string resolve_elf_path(const std::string& path) {
if (path.ends_with(".elf")) { if (path.ends_with(".elf")) {
return path; 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 = { constexpr ElfRequirements EXECUTABLE_REQUIREMENTS = {
+9
View File
@@ -9,6 +9,7 @@ tactility_add_module(app-module
PRIV_INCLUDE_DIRS private/ PRIV_INCLUDE_DIRS private/
INCLUDE_DIRS include/ INCLUDE_DIRS include/
REQUIRES TactilityKernel service-module minitar REQUIRES TactilityKernel service-module minitar
PRIV_REQUIRES TactilityKernelCpp
) )
# Tells source/io.cpp its real-syscall fallback must go through __real_read/write/close() # 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) tactility_get_module_name(app-module MODULE_NAME)
target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO) target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO)
endif () 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 * Computes the install directory for @a app_id (does not check whether anything is actually
* installed there). * installed there).
* @param[out] path always NULL-terminated on return, even on failure (empty string if * @param[out] path always NULL-terminated on return, even on failure. Empty when @a path_size
* @a path_size == 0 - nothing is written in that case; otherwise at least "" is written) * is 0, since nothing is written in that case.
* @retval ERROR_NONE on success * @retval ERROR_NONE on success
* @retval ERROR_BUFFER_OVERFLOW @a path_size is too small to hold the path (including the * @retval ERROR_BUFFER_OVERFLOW @a path_size is too small to hold the path (including the
* NULL terminator) * NULL terminator)
@@ -22,14 +22,15 @@ extern "C" {
error_t app_get_install_path(const char* app_id, char* path, size_t path_size); 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, * Installs a package from a tarball at @a source_path: extracts it into the package's install
* parses the extracted manifest.properties (see app/metadata.h) to determine its id, then * directory, parses the extracted manifest.properties (see app/package_manifest.h) into a
* registers it with app_manager_add() as an AppLocation{APP_LOCATION_PATH, <install dir>} app. * PackageManifest and one or more AppManifestBindings, then registers each with app_manager_add()
* If an app with the same id is already installed (via a previous app_install() call), it is * as an AppLocation{APP_LOCATION_PATH, <binary path>} app.
* uninstalled first - stopped if running, its old install directory removed - before the new * If a package with the same id is already installed (via a previous app_install() call), it is
* one takes its place. * uninstalled first: every one of its apps stopped if running, then its old install directory
* @param[in] source_path path to a tar file containing the app (must have manifest.properties * removed, before the new one takes its place.
* at its root) * @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_NONE on success
* @retval ERROR_NOT_FOUND @a source_path doesn't exist / can't be read * @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 * @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); error_t app_install(const char* source_path);
/** /**
* Uninstalls a previously app_install()-ed app: stops it if currently running, deletes its * Uninstalls a previously app_install()-ed package: stops every one of its apps if currently
* install directory, and unregisters it (app_manager_remove()). * running, deletes its install directory, and unregisters all of them (app_manager_remove()).
* @param[in] app_id the id the app was installed under (AppMetadata::app_id) * @param[in] app_id the package id it was installed under (PackageManifest::id)
* @retval ERROR_NONE on success * @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); error_t app_uninstall(const char* app_id);
+2 -2
View File
@@ -1,11 +1,11 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#pragma once #pragma once
#include "location.h"
#include <app/manifest.h> #include <app/manifest.h>
#include <tactility/error.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include "location.h" #include <tactility/error.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
+45 -2
View File
@@ -3,6 +3,7 @@
#include <app/instance.h> #include <app/instance.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/stream.h> #include <app/stream.h>
#include <tactility/error.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); typedef void (*AppManifestVisitorFn)(const struct AppManifest* manifest, void* context);
void app_manager_for_each_manifest(AppManifestVisitorFn visitor, 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 /** 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. */ * through to app_stream_subscribe() as-is; see its own doc for the ownership contracts. */
struct AppStreamBinding { 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 * 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 * @a path is expected to hold a manifest.properties (see app/package_manifest.h), matching the
* app_install() creates ({install dir}/{app_id}/manifest.properties), though this is not * 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 * 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 - * 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. * call app_manager_install_path_scan() to do that.
+11 -6
View File
@@ -11,7 +11,10 @@ extern "C" {
#endif #endif
// Character count, excluding null terminator // 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. */ /** Broad classification of an app, used for grouping/launcher presentation. */
enum AppCategory { enum AppCategory {
@@ -45,10 +48,10 @@ struct AppStackConfig {
/** Describes a registrable app. One manifest exists per app id. */ /** Describes a registrable app. One manifest exists per app id. */
struct AppManifest { struct AppManifest {
/** Unique app identifier. Should never be NULL. */ /** Unique app identifier. Must be NULL-terminated. */
const char* id; char id[APP_MANIFEST_ID_LENGTH + 1];
/** Human-readable name. Should never be NULL. */ /** Human-readable name. Must be NULL-terminated. */
const char* name; char name[APP_MANIFEST_NAME_LENGTH + 1];
enum AppCategory category; enum AppCategory category;
struct AppLocation location; struct AppLocation location;
/** Bitmask of AppManifestFlags. Most apps should leave this 0. */ /** Bitmask of AppManifestFlags. Most apps should leave this 0. */
@@ -57,7 +60,9 @@ struct AppManifest {
struct AppStackConfig stack; 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 #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/instance.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/private/fd_table.h> #include <app/private/fd_table.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h> #include <tactility/concurrent/mutex.h>
#include <tactility/freertos/freertos.h> #include <tactility/freertos/freertos.h>
#include <tactility/freertos/semphr.h> #include <tactility/freertos/semphr.h>
@@ -13,6 +16,7 @@
#include <stdint.h> #include <stdint.h>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector>
/** /**
* A dedicated completion signal(1) for one app instance's task, given as the * A dedicated completion signal(1) for one app instance's task, given as the
@@ -57,8 +61,16 @@ struct AppInstanceRecord {
AppFdTable fd_table {}; 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 { struct AppLedger {
std::unordered_map<std::string, const AppManifest*> manifests; 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; std::unordered_map<uint32_t, AppInstanceRecord> instances;
uint32_t next_instance_id = 1; uint32_t next_instance_id = 1;
Mutex mutex {}; 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/install.h>
#include <app/manager.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/fs.h>
#include <app/private/ledger.h> #include <app/private/ledger.h>
#include <app/private/package_manifest_parsing.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h> #include <tactility/concurrent/mutex.h>
#include <tactility/log.h> #include <tactility/log.h>
@@ -29,7 +33,7 @@ constexpr auto* TAG = "app_install";
namespace { 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) // app_metadata_parsing.cpp for the same constraint applied to properties-file loading)
std::string last_path_segment(const std::string& path) { 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 // endregion
// region Staging-path lock: at most one caller may clean up/populate a given staging_path at a // 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() // 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, one call's cleanup // concurrently, e.g. two uploads sharing a source basename; without this lock, one call's
// can delete or overwrite the staging directory another call is still extracting into. // cleanup could delete or overwrite the staging directory another call is still extracting into.
struct StagingLock { struct StagingLock {
Mutex mutex {}; Mutex mutex {};
@@ -223,18 +227,19 @@ void release_staging_lock(const std::string& path) {
// endregion // endregion
// region Installed-app registry: owns the AppManifest (and its id/name/path strings) that // region Installed-package registry: owns the AppManifests (and their backing location-path
// app_manager's ledger only keeps a non-owning pointer to (see app_manager_add()'s contract). // 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 { struct InstalledPackageRecord {
std::string id; std::string path; // install directory
std::string name; std::vector<AppManifest> manifests; // one per AppManifest the package declared
std::string path; std::vector<std::string> locations; // backs manifests[i].location.location, same indices
AppManifest manifest {};
}; };
struct InstallRegistry { struct InstallRegistry {
std::unordered_map<std::string, std::unique_ptr<InstalledAppRecord>> apps; std::unordered_map<std::string, std::unique_ptr<InstalledPackageRecord>> apps;
Mutex mutex {}; Mutex mutex {};
InstallRegistry() { mutex_construct(&mutex); } InstallRegistry() { mutex_construct(&mutex); }
@@ -245,46 +250,68 @@ InstallRegistry& install_registry() {
return registry; return registry;
} }
// Registers @a app_dir_path (already confirmed to hold a valid manifest.properties, parsed into // Registers every AppManifest in @a bindings (@a count of them) with app_manager_add(), taking
// @a metadata) with app_manager_add(), taking ownership of its id/name/path strings. // ownership of their backing location strings, then registers @a package itself.
// @warning Caller must hold install_registry().mutex, and must have already ensured // @warning Caller must hold install_registry().mutex.
// @a metadata.app_id isn't already registered (app_manager_add() rejects duplicates, but the error_t register_installed_package_locked(const PackageManifest& package, const std::string& install_path, const AppManifestBinding* bindings, size_t count) {
// 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) {
auto& registry = install_registry(); auto& registry = install_registry();
auto record = std::make_unique<InstalledAppRecord>(); auto record = std::make_unique<InstalledPackageRecord>();
record->id = metadata.app_id; record->path = install_path;
record->name = metadata.app_name; // Sized once, up front: manifests[i].location.location points into locations[i].c_str(),
record->path = app_dir_path; // which would dangle if either vector reallocated afterward.
record->manifest = AppManifest { record->manifests.resize(count);
.id = record->id.c_str(), record->locations.resize(count);
.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 },
};
// Belt-and-braces: app_install()'s earlier app_manager_remove() call is meant to have for (size_t i = 0; i < count; i++) {
// already cleared any stale registration for this id (e.g. left over from record->manifests[i] = bindings[i].manifest;
// app_manager_install_path_scan()'s separate registry), but that call happens before the record->locations[i] = app_resolve_binary_path(install_path, bindings[i].binary);
// tarball is even extracted - remove once more, right before add, so a duplicate id can record->manifests[i].location = { APP_LOCATION_PATH, const_cast<char*>(record->locations[i].c_str()) };
// 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;
} }
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; return ERROR_NONE;
} }
// Stops every currently-running instance of @a manifest. Collects matching instance ids while // 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 // 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. // the instance's own thread_main()) is held, or the two threads would deadlock each other.
void stop_all_instances_of(const AppManifest* manifest) { 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 // 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& registry = install_registry();
auto iterator = registry.apps.find(app_id); auto iterator = registry.apps.find(package_id);
if (iterator == registry.apps.end()) { if (iterator == registry.apps.end()) {
return ERROR_NOT_FOUND; return ERROR_NOT_FOUND;
} }
// Can't uninstall in-memory apps for (const auto& manifest : iterator->second->manifests) {
if (iterator->second->manifest.location.type != APP_LOCATION_PATH) { // Can't uninstall in-memory apps
return ERROR_NOT_SUPPORTED; if (manifest.location.type != APP_LOCATION_PATH) {
continue;
}
stop_all_instances_of(&manifest);
app_manager_remove(manifest.id);
} }
app_manager_remove_package(package_id.c_str());
stop_all_instances_of(&iterator->second->manifest);
app_manager_remove(app_id.c_str());
delete_recursively(iterator->second->path); delete_recursively(iterator->second->path);
registry.apps.erase(iterator); 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) { 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; std::string app_parent_path;
if (!get_app_install_directory(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; return ERROR_INVALID_ARGUMENT;
} }
AppMetadata metadata {}; PackageManifest package {};
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) { 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"); LOG_E(TAG, "Install failed: invalid manifest");
delete_recursively(staging_path); delete_recursively(staging_path);
release_staging_lock(staging_path); release_staging_lock(staging_path);
@@ -400,24 +430,13 @@ error_t app_install(const char* source_path) {
auto& registry = install_registry(); auto& registry = install_registry();
mutex_lock(&registry.mutex); mutex_lock(&registry.mutex);
// Replace any previous install of this app id (mirrors the old install()'s "already // Replace any previous installation of this package - this also handles the app_manager
// running/present" handling). uninstall_locked() only clears app_install.cpp's own // registrations of its old AppManifests, so there's no separate app_manager_remove() needed
// registry - the same app id may instead be registered by app_manager_install_path_scan() // here (register_installed_package_locked() below still defends against a stale registration
// (manager.cpp's separate registry, scanning this same directory tree), which // per individual id, e.g. one left by app_manager_install_path_scan()'s separate registry).
// uninstall_locked() doesn't know about. Clear the app-manager registration unconditionally uninstall_locked(package.id);
// too, or app_manager_add() below rejects the re-add as a duplicate.
uninstall_locked(metadata.app_id);
error_t remove_result = app_manager_remove(metadata.app_id); auto final_path = app_parent_path + "/" + package.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;
delete_recursively(final_path); delete_recursively(final_path);
if (rename(staging_path.c_str(), final_path.c_str()) != 0) { 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); release_staging_lock(staging_path);
// Only remaining failure mode is a duplicate id - can't happen, uninstall_locked() above // app_bindings.size(), not package.app_manifest_count - the safe bound to index by.
// already removed any previous registration for this exact id. error_t add_result = register_installed_package_locked(package, final_path, app_bindings.data(), app_bindings.size());
error_t add_result = register_installed_app_locked(final_path, metadata);
mutex_unlock(&registry.mutex); mutex_unlock(&registry.mutex);
return add_result; return add_result;
+174 -44
View File
@@ -1,13 +1,17 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#include <app/manager.h> #include <app/manager.h>
#include <app/metadata.h> #include <app/package_manifest.h>
#include <app/private/arguments.h> #include <app/private/arguments.h>
#include <app/private/binary_path.h>
#include <app/private/fd_table.h> #include <app/private/fd_table.h>
#include <app/private/fs.h> #include <app/private/fs.h>
#include <app/private/ledger.h> #include <app/private/ledger.h>
#include <app/private/manager_internal.h> #include <app/private/manager_internal.h>
#include <app/private/package_manifest_parsing.h>
#include <app/private/scheduler.h> #include <app/private/scheduler.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h> #include <tactility/concurrent/mutex.h>
#include <tactility/error.h> #include <tactility/error.h>
#include <tactility/log.h> #include <tactility/log.h>
@@ -72,6 +76,67 @@ void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context)
mutex_unlock(&ledger.mutex); 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) { 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); char** argv = app_arguments_copy(argc, argv_in);
if (argc > 0 && argv == nullptr) { if (argc > 0 && argv == nullptr) {
@@ -200,19 +265,19 @@ error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size) {
namespace { namespace {
// Owns the AppManifest (and its id/name/path strings) that app_manager_add() only keeps a // Owns the AppManifests (and their backing location-path strings) that app_manager_add() only
// non-owning pointer to. Separate from app_install.cpp's registry: scanning only // keeps non-owning pointers to. Separate from app_install.cpp's registry: scanning only
// adds/removes registrations, never touches disk or running instances. // adds/removes registrations, never touches disk or running instances. AppManifest::id/name own
struct ScannedAppManifest { // their own storage directly (fixed arrays), so this doesn't need to separately own those.
std::string id; struct ScannedPackageManifest {
std::string name; std::string path; // scanned directory
std::string path; std::vector<AppManifest> manifests; // one per AppManifest the package declared
AppManifest manifest {}; std::vector<std::string> locations; // backs manifests[i].location.location, same indices
}; };
struct InstallPathRegistry { struct InstallPathRegistry {
std::vector<std::string> paths; 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 {}; Mutex mutex {};
InstallPathRegistry() { mutex_construct(&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); 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); 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) { 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); mutex_unlock(&registry.mutex);
// Parses without registry.mutex held; filesystem IO is slow. // 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) { for (const auto& app_dir : found_app_dirs) {
auto manifest_path = app_dir + "/manifest.properties"; auto manifest_path = app_dir + "/manifest.properties";
if (!app_fs_is_file(manifest_path)) { if (!app_fs_is_file(manifest_path)) {
continue; continue;
} }
AppMetadata metadata {}; PackageManifest package {};
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) { // 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()); LOG_W(TAG, "Invalid manifest at %s", manifest_path.c_str());
continue; continue;
} }
if (known_paths.contains(metadata.app_id)) { if (known_packages.contains(package.id)) {
continue; continue;
} }
auto record = std::make_unique<ScannedAppManifest>(); auto record = std::make_unique<ScannedPackageManifest>();
record->id = metadata.app_id;
record->name = metadata.app_name;
record->path = app_dir; record->path = app_dir;
record->manifest = AppManifest { // Sized once, up front: manifests[i].location.location points into locations[i].c_str(),
.id = record->id.c_str(), // which would dangle if either vector reallocated afterward.
.name = record->name.c_str(), record->manifests.resize(app_bindings.size());
.category = APP_CATEGORY_USER, record->locations.resize(app_bindings.size());
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) }, for (size_t i = 0; i < app_bindings.size(); i++) {
.flags = 0, record->manifests[i] = app_bindings[i].manifest;
.stack = { .depth = static_cast<uint16_t>(metadata.stack_depth), .desired_memory_capability = 0 }, 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)); new_records.push_back(std::move(record));
} }
std::vector<std::string> missing_ids; std::vector<std::string> missing_ids;
for (const auto& [id, path] : known_paths) { for (const auto& [id, known] : known_packages) {
if (!app_fs_is_directory(path)) { if (!app_fs_is_directory(known.path)) {
missing_ids.push_back(id); 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 would fix a lock order an opposite-order caller could deadlock against.
// registry.mutex is retaken afterward only to publish the in-memory results. // registry.mutex is retaken afterward only to publish the in-memory results.
for (const auto& id : missing_ids) { for (const auto& id : missing_ids) {
app_manager_remove(id.c_str()); for (const auto& manifest_id : known_packages.at(id).manifest_ids) {
} app_manager_remove(manifest_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());
} }
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); mutex_lock(&registry.mutex);
for (const auto& id : missing_ids) { for (const auto& id : missing_ids) {
registry.scanned.erase(id); registry.scanned.erase(id);
} }
for (auto& record : added_records) { for (size_t i = 0; i < added_records.size(); i++) {
registry.scanned[record->id] = std::move(record); registry.scanned[added_package_ids[i]] = std::move(added_records[i]);
} }
mutex_unlock(&registry.mutex); mutex_unlock(&registry.mutex);
} }
@@ -332,7 +453,10 @@ error_t app_manager_install_path_uninstall(const char* app_id) {
return ERROR_NOT_FOUND; 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; auto path = iterator->second->path;
mutex_unlock(&registry.mutex); mutex_unlock(&registry.mutex);
@@ -342,8 +466,11 @@ error_t app_manager_install_path_uninstall(const char* app_id) {
auto& ledger = app_ledger(); auto& ledger = app_ledger();
mutex_lock(&ledger.mutex); mutex_lock(&ledger.mutex);
for (const auto& [id, record] : ledger.instances) { for (const auto& [id, record] : ledger.instances) {
if (record.manifest == manifest) { for (const auto& manifest : *manifests) {
instance_ids.push_back(id); if (record.manifest == &manifest) {
instance_ids.push_back(id);
break;
}
} }
} }
mutex_unlock(&ledger.mutex); 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 // app_manager_remove() takes ledger.mutex; call outside registry.mutex too, matching
// the lock order in app_manager_install_path_scan(). // 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 // Delete before erasing the scan record, so a failed deletion still leaves the
// entry discoverable for a retry. // entry discoverable for a retry.
+18 -3
View File
@@ -1,15 +1,30 @@
#include <app/manifest.h> #include <app/manifest.h>
#include <app/private/metadata_parsing_internal.h> #include <app/private/package_manifest_parsing.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
extern "C" { extern "C" {
bool app_id_is_valid(const char* id) { bool app_manifest_id_is_valid(const char* id) {
auto size = strlen(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 == '.'; 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/io.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/metadata.h> #include <app/package_manifest.h>
#include <app/paths.h> #include <app/paths.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.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_for_each_manifest),
DEFINE_MODULE_SYMBOL(app_manager_add), DEFINE_MODULE_SYMBOL(app_manager_add),
DEFINE_MODULE_SYMBOL(app_manager_remove), 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_instance_id),
DEFINE_MODULE_SYMBOL(app_manager_get_topmost_app_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_add),
DEFINE_MODULE_SYMBOL(app_manager_install_path_scan), DEFINE_MODULE_SYMBOL(app_manager_install_path_scan),
DEFINE_MODULE_SYMBOL(app_manager_install_path_uninstall), 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 // app/manifest
DEFINE_MODULE_SYMBOL(app_id_is_valid), DEFINE_MODULE_SYMBOL(app_manifest_id_is_valid),
// app/metadata DEFINE_MODULE_SYMBOL(app_manifest_name_is_valid),
DEFINE_MODULE_SYMBOL(app_metadata_parse), DEFINE_MODULE_SYMBOL(app_manifest_stack_size_is_valid),
// app/package_manifest
DEFINE_MODULE_SYMBOL(app_package_manifest_parse),
// app/paths // app/paths
DEFINE_MODULE_SYMBOL(app_paths_get_user_data_directory), DEFINE_MODULE_SYMBOL(app_paths_get_user_data_directory),
DEFINE_MODULE_SYMBOL(app_paths_get_user_data_path), 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), DEFINE_MODULE_SYMBOL(app_paths_get_assets_path),
// app/scheduler // app/scheduler
DEFINE_MODULE_SYMBOL(app_scheduler_current_app_id), 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 // app/stream
DEFINE_MODULE_SYMBOL(app_stream_subscribe), DEFINE_MODULE_SYMBOL(app_stream_subscribe),
DEFINE_MODULE_SYMBOL(app_stream_unsubscribe), 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 <tactility/paths.h>
#include <cstdio> #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" { 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; 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) { if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW; 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; 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. // manifest built directly in C++ (not parsed from a file) must be held to it too.
if (stack.depth > APP_STACK_SIZE_MAX) { 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); 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) { AppInstanceId start_idle_app(const char* id) {
ensure_memory_loader_registered(); 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); REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
AppInstanceId instance_id = 0; AppInstanceId instance_id = 0;
REQUIRE_EQ(app_start(id, 0, nullptr, &instance_id), ERROR_NONE); 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 #endif
} }
// location.location can be either an app's install directory or the .so file directly; the // location.location can be either an app's install directory or the .so file directly, mirroring
// former resolves to the per-architecture binary at {dir}/elf/posix-{TACTILITY_POSIX_ARCH}.so, // app_esp32_loader_service.cpp's resolve_elf_path(). A "packaged" app's install directory always
// mirroring app_esp32_loader_service.cpp's resolve_elf_path(). // 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) { error_t resolve_app_path(const std::string& path, std::string& resolvedPath) {
if (path.ends_with(".so")) { if (path.ends_with(".so")) {
resolvedPath = path; resolvedPath = path;
return ERROR_NONE; return ERROR_NONE;
} }
std::string shared_object_path = path + "/elf/posix-" TACTILITY_POSIX_ARCH ".so"; std::string candidate = path + "/bin/posix-" TACTILITY_POSIX_ARCH "/app.so";
if (!is_regular_file(shared_object_path)) { if (!is_regular_file(candidate)) {
return ERROR_NOT_FOUND; return ERROR_NOT_FOUND;
} }
resolvedPath = shared_object_path; resolvedPath = candidate;
return ERROR_NONE; 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()); 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); void* handle = dlopen(app_path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (handle == nullptr) { if (handle == nullptr) {
LOG_E(TAG, "dlopen(%s) failed: %s", app_path.c_str(), dlerror()); 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[]) { int32_t api_run(AppRuntime runtime_ptr, uint32_t /*app_instance_id*/, int argc, char* argv[]) {
auto* runtime = static_cast<PosixAppRuntime*>(runtime_ptr); auto* runtime = static_cast<PosixAppRuntime*>(runtime_ptr);
dlerror(); // clear any pending error, per dlsym(3)'s own recommended idiom for telling a NULL // Clear any pending error, per dlsym(3)'s own recommended idiom for telling a NULL symbol address apart from a real lookup failure
// symbol address apart from a real lookup failure dlerror();
void* symbol = dlsym(runtime->handle, "main"); void* symbol = dlsym(runtime->handle, "main");
const char* lookup_error = dlerror(); const char* lookup_error = dlerror();
if (symbol == nullptr || lookup_error != nullptr) { 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") set(NON_ELF_FIXTURE_PATH "${CMAKE_CURRENT_BINARY_DIR}/not-elf.so")
file(WRITE "${NON_ELF_FIXTURE_PATH}" "not an elf file") 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) file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
add_executable(AppPosixModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES}) 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_include_directories(AppPosixModuleTests PRIVATE ${DOCTESTINC})
target_compile_definitions(AppPosixModuleTests PRIVATE target_compile_definitions(AppPosixModuleTests PRIVATE
FIXTURE_APP_PATH="$<TARGET_FILE:app_posix_module_test_fixture>" FIXTURE_APP_PATH="$<TARGET_FILE:app_posix_module_test_fixture>"
FIXTURE_NON_ELF_PATH="${NON_ELF_FIXTURE_PATH}" FIXTURE_NON_ELF_PATH="${NON_ELF_FIXTURE_PATH}"
FIXTURE_INSTALL_DIR_PATH="${INSTALL_DIR_FIXTURE_PATH}"
) )
add_test(NAME AppPosixModuleTests COMMAND AppPosixModuleTests) 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") { TEST_CASE("app_is_executable() rejects an install-directory-shaped path missing its per-arch .so") {
ensure_path_loader_registered(); 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())); 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) { 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); return std::format("{}/{}", getAppInstallPath(), appId);
} }
std::string getAppUserPath(const std::string& 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); return std::format("{}/app/{}", getUserHomePath(), appId);
} }
+5 -5
View File
@@ -26,9 +26,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/module.h> #include <app/module.h>
#include <app/start.h>
#include <Tactility/Tactility.h> #include <Tactility/Tactility.h>
@@ -150,9 +150,9 @@ namespace app {
namespace alertdialog { extern const ::AppManifest manifest; } namespace alertdialog { extern const ::AppManifest manifest; }
namespace apphub { extern const ::AppManifest manifest; } namespace apphub { extern const ::AppManifest manifest; }
namespace apphubdetails { 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 applist { extern const ::AppManifest manifest; }
namespace appsettings { extern const ::AppManifest manifest; } namespace apppackagelist { extern const ::AppManifest manifest; }
namespace audiosettings { extern const ::AppManifest manifest; } namespace audiosettings { extern const ::AppManifest manifest; }
namespace boot { extern const ::AppManifest manifest; } namespace boot { extern const ::AppManifest manifest; }
namespace development { extern const ::AppManifest manifest; } namespace development { extern const ::AppManifest manifest; }
@@ -212,11 +212,11 @@ static void registerInternalApps() {
LOG_I(TAG, "Registering internal apps"); LOG_I(TAG, "Registering internal apps");
app_manager_add(&app::alertdialog::manifest); 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::apphub::manifest);
app_manager_add(&app::apphubdetails::manifest); app_manager_add(&app::apphubdetails::manifest);
app_manager_add(&app::applist::manifest); app_manager_add(&app::applist::manifest);
app_manager_add(&app::appsettings::manifest); app_manager_add(&app::apppackagelist::manifest);
if (service::audio::isAvailable()) { if (service::audio::isAvailable()) {
app_manager_add(&app::audiosettings::manifest); app_manager_add(&app::audiosettings::manifest);
} }
@@ -2,9 +2,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -7,11 +7,11 @@
#include <app/event.h> #include <app/event.h>
#include <app/install.h> #include <app/install.h>
#include <app/metadata.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <http/download.h> #include <http/download.h>
@@ -204,9 +204,9 @@ void updateViews(Context* ctx) {
if (is_installed) { if (is_installed) {
std::string metadata_path = std::string(install_path) + "/manifest.properties"; std::string metadata_path = std::string(install_path) + "/manifest.properties";
AppMetadata metadata; PackageManifest package;
if (app_metadata_parse(metadata_path.c_str(), &metadata) == ERROR_NONE if (app_package_manifest_parse(metadata_path.c_str(), &package, nullptr, 0) == ERROR_NONE
&& metadata.app_version_code < ctx->entry.appVersionCode) { && package.version_code < ctx->entry.appVersionCode) {
ctx->updateButton = lvgl_toolbar_add_image_button_action(ctx->toolbar, LV_SYMBOL_DOWNLOAD, onUpdatePressed, ctx); 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); lv_obj_remove_flag(ctx->updateLabel, LV_OBJ_FLAG_HIDDEN);
} }
+1 -1
View File
@@ -1,8 +1,8 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -1,9 +1,10 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h>
#include <app/install.h> #include <app/install.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <format> #include <format>
@@ -12,17 +13,15 @@
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
#include <Tactility/StringUtils.h>
#include <Tactility/app/alertdialog/AlertDialog.h> #include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/file/File.h>
#include <Tactility/lvgl/Style.h> #include <Tactility/lvgl/Style.h>
#include <tactility/check.h> #include <tactility/check.h>
#include <tactility/log.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; extern const ::AppManifest manifest;
@@ -30,13 +29,34 @@ namespace {
struct Context { struct Context {
uint32_t appInstanceId; uint32_t appInstanceId;
std::string targetAppId; std::string targetPackageId;
// findAppManifestById() returns the old-model registry's AppManifest type - AppDetails PackageManifest targetPackage = {};
// shows details for apps in that registry regardless of which system they run under. std::vector<std::string> appIds;
AppManifest targetManifest = { };
uint32_t pendingUninstallDialogId = 0; 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) { void onPressUninstall(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(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->pendingUninstallDialogId = alertdialog::start(
ctx->appInstanceId, ctx->appInstanceId,
"Confirmation", "Confirmation",
std::format("Uninstall {}?", ctx->targetManifest.name), std::format("Uninstall {}?", ctx->targetPackage.id),
choices 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_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); 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()); auto* toolbar = lvgl_toolbar_create(parent, title.c_str());
// The global toolbar nav callback only knows how to stop old-model apps. // The global toolbar nav callback only knows how to stop old-model apps.
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); 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); lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
lvgl::obj_set_style_bg_invisible(wrapper); 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); auto* identifier_label = lv_label_create(wrapper);
lv_label_set_text(identifier_label, identifier.c_str()); lv_label_set_text(identifier_label, identifier.c_str());
auto* location_label = lv_label_create(wrapper); auto version = std::format("Version: {} ({})", ctx->targetPackage.version_name, ctx->targetPackage.version_code);
std::string location; auto* version_label = lv_label_create(wrapper);
bool is_internal = ctx->targetManifest.location.type == APP_LOCATION_MEMORY; lv_label_set_text(version_label, version.c_str());
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());
if (is_external) { char install_path[192];
auto* uninstall_button = lv_button_create(wrapper); std::string location = "unknown";
lv_obj_set_width(uninstall_button, LV_PCT(100)); if (app_get_install_path(ctx->targetPackage.id, install_path, sizeof(install_path)) == ERROR_NONE) {
lv_obj_add_event_cb(uninstall_button, onPressUninstall, LV_EVENT_SHORT_CLICKED, ctx); location = install_path;
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");
} }
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[]) { int32_t appMain(int argc, char* argv[]) {
@@ -107,9 +131,9 @@ int32_t appMain(int argc, char* argv[]) {
Context ctx {}; Context ctx {};
ctx.appInstanceId = appInstanceId; ctx.appInstanceId = appInstanceId;
ctx.targetAppId = (argc > 0) ? argv[0] : std::string(); ctx.targetPackageId = (argc > 0) ? argv[0] : std::string();
if (app_manager_find_manifest(ctx.targetAppId.c_str(), &ctx.targetManifest) != ERROR_NONE) { if (!findPackage(ctx.targetPackageId, ctx.targetPackage, ctx.appIds)) {
LOG_W(TAG, "App %s not found", ctx.targetAppId.c_str()); LOG_W(TAG, "Package %s not found", ctx.targetPackageId.c_str());
return 0; return 0;
} }
@@ -134,7 +158,7 @@ int32_t appMain(int argc, char* argv[]) {
case APP_EVENT_RESULT: case APP_EVENT_RESULT:
if (event.result.launch_id == ctx.pendingUninstallDialogId) { if (event.result.launch_id == ctx.pendingUninstallDialogId) {
if (event.result.result == 0) { // 0 = Yes if (event.result.result == 0) { // 0 = Yes
app_uninstall(ctx.targetManifest.id); app_uninstall(ctx.targetPackage.id);
shouldClose = true; shouldClose = true;
} }
app_manager_stop(event.result.launch_id); app_manager_stop(event.result.launch_id);
@@ -156,15 +180,15 @@ int32_t appMain(int argc, char* argv[]) {
} // namespace } // namespace
void start(const std::string& appId) { void start(const std::string& packageId) {
const char* argv[] = { appId.c_str() }; const char* argv[] = { packageId.c_str() };
uint32_t instanceId = 0; uint32_t instanceId = 0;
app_start(manifest.id, 1, argv, &instanceId); app_start(manifest.id, 1, argv, &instanceId);
} }
extern const ::AppManifest manifest = { extern const ::AppManifest manifest = {
.id = "tactility.appdetails", .id = "tactility.apppackagedetails",
.name = "App Details", .name = "Package Details",
.category = APP_CATEGORY_SYSTEM, .category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }, .location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN, .flags = APP_MANIFEST_FLAG_HIDDEN,
@@ -1,11 +1,12 @@
#include <lvgl/icons/shared.h> #include <lvgl/icons/shared.h>
#include <lvgl/fonts.h> #include <lvgl/fonts.h>
#include <Tactility/app/appdetails/AppDetails.h> #include <Tactility/app/apppackagedetails/AppPackageDetails.h>
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -18,7 +19,7 @@
#include <cstring> #include <cstring>
#include <vector> #include <vector>
namespace tt::app::appsettings { namespace tt::app::apppackagelist {
extern const ::AppManifest manifest; extern const ::AppManifest manifest;
@@ -26,11 +27,13 @@ namespace {
struct Context { struct Context {
uint32_t appInstanceId; 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) { void onPackagePressed(lv_event_t* e) {
const auto* target_manifest = static_cast<const ::AppManifest*>(lv_event_get_user_data(e)); auto* packageId = static_cast<char*>(lv_event_get_user_data(e));
appdetails::start(target_manifest->id); apppackagedetails::start(packageId);
} }
void onBackPressed(lv_event_t* event) { void onBackPressed(lv_event_t* event) {
@@ -38,18 +41,16 @@ void onBackPressed(lv_event_t* event) {
app_event_emit_close(ctx->appInstanceId); app_event_emit_close(ctx->appInstanceId);
} }
void createAppWidget(const ::AppManifest* target_manifest, lv_obj_t* list) { void createPackageWidget(const char* packageId, lv_obj_t* list) {
// The new AppManifest has no per-app icon - use a shared generic one for every entry, same lv_obj_t* btn = lv_list_add_button(list, LVGL_ICON_SHARED_TOOLBAR, packageId);
// fallback AppList.cpp uses.
lv_obj_t* btn = lv_list_add_button(list, LVGL_ICON_SHARED_TOOLBAR, target_manifest->name);
lv_obj_t* image = lv_obj_get_child(btn, 0); 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_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) { void collectPackageId(const ::AppPackage* pkg, void* context) {
auto* manifests = static_cast<std::vector<const ::AppManifest*>*>(context); auto* packageIds = static_cast<std::vector<std::string>*>(context);
manifests->push_back(manifest); packageIds->emplace_back(pkg->package.id);
} }
void createWidgets(lv_obj_t* parent, void* userData) { 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_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1); lv_obj_set_flex_grow(list, 1);
std::vector<const ::AppManifest*> manifests; // createWidgets() can rerun for this same Context (window rebuild-on-remove).
app_manager_for_each_manifest(collectManifest, &manifests); ctx->packageIds.clear();
std::ranges::sort(manifests, [](const ::AppManifest* a, const ::AppManifest* b) { app_manager_for_each_package(collectPackageId, &ctx->packageIds);
return strcmp(a->name, b->name) < 0; std::ranges::sort(ctx->packageIds);
});
size_t app_count = 0; for (const auto& packageId : ctx->packageIds) {
for (const auto* target_manifest: manifests) { createPackageWidget(packageId.c_str(), list);
if (target_manifest->location.type == APP_LOCATION_PATH) {
app_count++;
createAppWidget(target_manifest, 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 // 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 // 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. // 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 } // namespace
extern const ::AppManifest manifest = { extern const ::AppManifest manifest = {
.id = "tactility.appsettings", .id = "tactility.apppackagelist",
.name = "Apps", .name = "Apps",
.category = APP_CATEGORY_SETTINGS, .category = APP_CATEGORY_SETTINGS,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) }, .location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
+1 -1
View File
@@ -10,9 +10,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
+1 -1
View File
@@ -5,9 +5,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -10,9 +10,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -11,8 +11,8 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/start.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -7,9 +7,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -6,9 +6,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/io.h> #include <app/io.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <app/stream.h> #include <app/stream.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -8,9 +8,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -7,10 +7,10 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/paths.h> #include <app/paths.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -6,9 +6,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -2,9 +2,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <app/stream.h> #include <app/stream.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
+1 -1
View File
@@ -1,8 +1,8 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <cstring> #include <cstring>
+1 -1
View File
@@ -7,9 +7,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <app/stream.h> #include <app/stream.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -2,9 +2,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
+1 -1
View File
@@ -1,8 +1,8 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
+1 -1
View File
@@ -10,9 +10,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -5,9 +5,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
+1 -1
View File
@@ -8,9 +8,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -5,9 +5,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -6,9 +6,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -6,9 +6,9 @@
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
#include <app/start.h>
#include <app/manifest.h> #include <app/manifest.h>
#include <app/scheduler.h> #include <app/scheduler.h>
#include <app/start.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@@ -190,7 +190,7 @@ error_t DevelopmentService::handleAppInstall(HttpServerRequest* request, void*)
if ( if (
name_entry == content_disposition_map.end() || name_entry == content_disposition_map.end() ||
filename_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"); http_server_request_send_error(request, 400, "Multipart form error: name or filename parameter missing or mismatching");
return ERROR_UNDEFINED; return ERROR_UNDEFINED;
+8 -6
View File
@@ -1,7 +1,9 @@
manifest.version=0.2 manifest.version=0.3
target.sdk=0.0.0 target.sdk=0.8.0-dev
target.platforms=esp32,esp32s3,esp32c6,esp32p4,posix-x86_64 target.platforms=esp32,esp32s3,esp32c6,esp32p4,posix-x86_64
app.id=tactility.sdktest id=tactility.sdktest
app.version.name=0.1.0 version.name=0.1.0
app.version.code=1 version.code=1
app.name=SDK Test 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 from urllib.parse import urlparse
ttbuild_path = ".tactility" ttbuild_path = ".tactility"
ttbuild_version = "5.0.1" ttbuild_version = "6.0.0"
ttbuild_cdn = "https://cdn.tactilityproject.org" ttbuild_cdn = "https://cdn.tactilityproject.org"
ttbuild_sdk_json_validity = 3600 # seconds ttbuild_sdk_json_validity = 3600 # seconds
ttport = 6666 ttport = 6666
@@ -21,7 +21,7 @@ use_local_sdk = False
local_base_path = None local_base_path = None
http_timeout_seconds = 10 http_timeout_seconds = 10
# App install uploads the whole package over HTTP and the device only responds once it's # 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. # easily take well over http_timeout_seconds on a slow SD card, so give it a lot more room.
install_timeout_seconds = 120 install_timeout_seconds = 120
@@ -33,30 +33,55 @@ shell_color_cyan = "\033[36m"
shell_color_reset = "\033[m" shell_color_reset = "\033[m"
def print_help(): def print_help():
print("Usage: python tactility.py [app_path] [action] [options]") print("Usage: python tactility.py [action] [options]")
print("") print("")
print("Actions:") 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 lower case. Example: esp32s3")
print(" Supported platforms are read from manifest.properties") print(" Supported platforms are read from manifest.properties")
print(" Parameters:")
print(" (optional) --architecture [input], -a [input]")
print("")
print(" clean Clean the build folders") print(" clean Clean the build folders")
print("")
print(" clearcache Clear the SDK cache") print(" clearcache Clear the SDK cache")
print("")
print(" updateself Update this tool") print(" updateself Update this tool")
print(" run [ip] Run the application") print("")
print(" install [ip] Install the application") print(" run Run the application")
print(" uninstall [ip] Uninstall the application") print(" Parameters:")
print(" bir [ip] [platform] Build, install then run. Optionally specify a platform.") print(" (required) --host, -h [input]")
print(" brrr [ip] [platform] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.") 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("")
print("Options:") print("Options:")
print(" -p, --path [input] Path to the app directory (defaults to current directory)")
print(" --help Show this commandline info") 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(" --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(" --skip-build Run everything except the idf.py/CMake commands")
print(" --verbose Show extra console output") print(" --verbose Show extra console output")
print("") print("")
print("Examples:") print("Examples:")
print(" python tactility.py Apps/Snake build esp32s3 --verbose") print(" python tactility.py build")
print(" python tactility.py Apps/Snake bir 192.168.1.50 esp32s3") 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 # region Core
@@ -105,8 +130,8 @@ def exit_with_error(message):
print_error(message) print_error(message)
sys.exit(1) sys.exit(1)
def get_url(ip, path): def get_url(host, path):
return f"http://{ip}:{ttport}{path}" return f"http://{host}:{ttport}{path}"
def read_properties_file(path): def read_properties_file(path):
properties = {} properties = {}
@@ -123,6 +148,126 @@ def read_properties_file(path):
#endregion Core #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 #region SDK helpers
def read_sdk_json(): 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)") exit_with_error("Server returned invalid SDK data format (toolDownloadUrl not found)")
tool_version = sdk_json["toolVersion"] tool_version = sdk_json["toolVersion"]
tool_compatibility = sdk_json["toolCompatibility"] 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"New version available: {tool_version} (currently using {ttbuild_version})")
print_warning(f"Run 'tactility.py updateself' to update.") print_warning(f"Run 'tactility.py updateself' to update.")
if re.search(tool_compatibility, ttbuild_version) is None: if re.search(tool_compatibility, ttbuild_version) is None:
@@ -247,10 +392,70 @@ def validate_self(sdk_json):
def read_manifest(): def read_manifest():
return read_properties_file("manifest.properties") 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): 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: if key not in manifest:
exit_with_error(f"Invalid manifest format: {key} not found") 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): def is_valid_manifest_platform(manifest, platform):
manifest_platforms = manifest["target.platforms"].split(",") manifest_platforms = manifest["target.platforms"].split(",")
@@ -345,7 +550,7 @@ tactility_project_post(%(app_id)s)
def cmakelists_version_marker(): def cmakelists_version_marker():
return f"# tactility-cmakelists-version: {CMAKELISTS_VERSION}" 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() marker = cmakelists_version_marker()
if os.path.exists("CMakeLists.txt"): if os.path.exists("CMakeLists.txt"):
with open("CMakeLists.txt", "r") as file: with open("CMakeLists.txt", "r") as file:
@@ -353,7 +558,7 @@ def ensure_cmakelists_up_to_date(manifest):
if first_line == marker: if first_line == marker:
return return
print(f"Updating CMakeLists.txt to {marker}") 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: with open("CMakeLists.txt", "w") as file:
file.write(content) file.write(content)
@@ -532,43 +737,58 @@ def package_intermediate_manifest(target_path):
shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties")) shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties"))
return True return True
def package_intermediate_binaries(target_path, platforms): def get_artifact_extension(platform):
elf_dir = os.path.join(target_path, "elf") # POSIX apps are dlopen()ed shared objects (app-posix-module), not idf.py/elf_loader
os.makedirs(elf_dir, exist_ok=True) # relocatable images, so they land as a plain ".so" instead of ".elf".
for platform in platforms: return ".so" if platform.startswith("posix") else ".elf"
elf_path = find_elf_file(platform)
if elf_path is None: def package_intermediate_binaries(target_path, platforms, manifest):
print_error(f"ELF file not found for {platform}") # Each binary is built in its own directory (get_binary_dirs()), with its own
return False # build/cmake-build-{platform} tree; chdir into it so find_elf_file()/get_cmake_path()
# app-posix-module's loader resolves an installed app to "elf/posix-<arch>.so", matching # (both CWD-relative) resolve against the right one, then restore CWD for the next binary.
# its own compile-time architecture, not "*.elf". original_cwd = os.getcwd()
extension = ".so" if platform.startswith("posix") else ".elf" for binary, directory in get_binary_dirs(manifest):
shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}{extension}")) 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 return True
def package_intermediate_assets(target_path): def package_intermediate_assets(target_path):
if os.path.isdir("assets"): if os.path.isdir("assets"):
shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True) shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True)
def package_intermediate(platforms): def package_intermediate(platforms, manifest):
target_path = os.path.join("build", "package-intermediate") target_path = os.path.abspath(os.path.join("build", "package-intermediate"))
if os.path.isdir(target_path): if os.path.isdir(target_path):
shutil.rmtree(target_path) shutil.rmtree(target_path)
os.makedirs(target_path, exist_ok=True) os.makedirs(target_path, exist_ok=True)
if not package_intermediate_manifest(target_path): if not package_intermediate_manifest(target_path):
return False return False
if not package_intermediate_binaries(target_path, platforms): if not package_intermediate_binaries(target_path, platforms, manifest):
return False return False
package_intermediate_assets(target_path) package_intermediate_assets(target_path)
return True return True
def package_name(manifest): 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): def package_all(manifest, platforms):
status = f"Building package with {platforms}" status = f"Building package with {platforms}"
print_status_busy(status) print_status_busy(status)
if not package_intermediate(platforms): if not package_intermediate(platforms, manifest):
print_status_error("Building package failed: missing inputs") print_status_error("Building package failed: missing inputs")
return False return False
# Create build/something.app # Create build/something.app
@@ -588,8 +808,9 @@ def setup_environment():
global ttbuild_path global ttbuild_path
os.makedirs(ttbuild_path, exist_ok=True) os.makedirs(ttbuild_path, exist_ok=True)
def build_action(manifest, platform_arg, skip_build): def build_action(manifest, arguments):
ensure_cmakelists_up_to_date(manifest) platform_arg = arguments.get("platform")
skip_build = arguments.has("skip-build")
platforms_to_build = get_manifest_target_platforms(manifest, platform_arg) platforms_to_build = get_manifest_target_platforms(manifest, platform_arg)
# Environment validation # Environment validation
validate_environment(platforms_to_build) validate_environment(platforms_to_build)
@@ -598,10 +819,10 @@ def build_action(manifest, platform_arg, skip_build):
global local_base_path global local_base_path
local_base_path = os.environ.get("TACTILITY_SDK_PATH") local_base_path = os.environ.get("TACTILITY_SDK_PATH")
validate_local_sdks(platforms_to_build, manifest["target.sdk"]) validate_local_sdks(platforms_to_build, manifest["target.sdk"])
if should_fetch_sdkconfig_files(platforms_to_build): if should_fetch_sdkconfig_files(platforms_to_build):
fetch_sdkconfig_files(platforms_to_build) fetch_sdkconfig_files(platforms_to_build)
if not use_local_sdk: if not use_local_sdk:
sdk_json = read_sdk_json() sdk_json = read_sdk_json()
validate_self(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 use_local_sdk:
if not sdk_download_all(sdk_version, platforms_to_build): if not sdk_download_all(sdk_version, platforms_to_build):
exit_with_error("Failed to download one or more SDKs") 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 skip_build:
if not package_all(manifest, platforms_to_build): if not package_all(manifest, platforms_to_build):
return False return False
return True 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"): if os.path.exists("build"):
print_status_busy("Removing build/") print_status_busy("Removing build/")
shutil.rmtree("build") shutil.rmtree("build")
print_status_success("Removed 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") print("Nothing to clean")
def clear_cache_action(): def clear_cache_action():
@@ -641,9 +892,9 @@ def update_self_action():
else: else:
exit_with_error("Update failed") exit_with_error("Update failed")
def get_device_info(ip): def get_device_info(host):
print_status_busy(f"Requesting device info") print_status_busy(f"Requesting device info")
url = get_url(ip, "/info") url = get_url(host, "/info")
try: try:
response = requests.get(url, timeout=http_timeout_seconds) response = requests.get(url, timeout=http_timeout_seconds)
if response.status_code != 200: if response.status_code != 200:
@@ -654,10 +905,13 @@ def get_device_info(ip):
except requests.RequestException as e: except requests.RequestException as e:
print_status_error(f"Device info request failed: {e}") print_status_error(f"Device info request failed: {e}")
def run_action(manifest, ip): def run_action(manifest, arguments):
app_id = manifest["app.id"] 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") print_status_busy("Running")
url = get_url(ip, "/app/run") url = get_url(host, "/app/run")
params = {'id': app_id} params = {'id': app_id}
try: try:
response = requests.post(url, params=params, timeout=http_timeout_seconds) response = requests.post(url, params=params, timeout=http_timeout_seconds)
@@ -668,21 +922,21 @@ def run_action(manifest, ip):
except requests.RequestException as e: except requests.RequestException as e:
print_status_error(f"Running request failed: {e}") print_status_error(f"Running request failed: {e}")
def install_action(manifest, ip, platforms): def install_action(manifest, arguments):
print_status_busy("Installing") host = arguments.get("host")
for platform in platforms: if host is None:
elf_path = find_elf_file(platform) exit_with_error("Missing required argument: --host")
if elf_path is None:
print_status_error(f"ELF file not built for {platform}")
return False
package_path = package_name(manifest) package_path = package_name(manifest)
# print(f"Installing {package_path} to {ip}") if not os.path.isfile(package_path):
url = get_url(ip, "/app/install") 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: try:
# Prepare multipart form data # Prepare multipart form data
with open(package_path, 'rb') as file: with open(package_path, 'rb') as file:
files = { files = {
'elf': file 'app': file
} }
response = requests.put(url, files=files, timeout=install_timeout_seconds) response = requests.put(url, files=files, timeout=install_timeout_seconds)
if response.status_code != 200: if response.status_code != 200:
@@ -698,10 +952,13 @@ def install_action(manifest, ip, platforms):
print_status_error(f"Install file error: {e}") print_status_error(f"Install file error: {e}")
return False return False
def uninstall_action(manifest, ip): def uninstall_action(manifest, arguments):
app_id = manifest["app.id"] 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") print_status_busy("Uninstalling")
url = get_url(ip, "/app/uninstall") url = get_url(host, "/app/uninstall")
params = {'id': app_id} params = {'id': app_id}
try: try:
response = requests.put(url, params=params, timeout=http_timeout_seconds) response = requests.put(url, params=params, timeout=http_timeout_seconds)
@@ -716,38 +973,34 @@ def uninstall_action(manifest, ip):
if __name__ == "__main__": if __name__ == "__main__":
print(f"Tactility Build System v{ttbuild_version}") 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() print_help()
sys.exit() sys.exit()
# Argument validation if parsed_command.action is None:
if len(sys.argv) == 1:
print_help() print_help()
sys.exit(1) sys.exit(1)
if "--verbose" in sys.argv:
verbose = True verbose = parsed_command.arguments.has("verbose")
sys.argv.remove("--verbose") use_local_sdk = parsed_command.arguments.has("local-sdk")
skip_build = False
if "--skip-build" in sys.argv: app_path = parsed_command.arguments.get("path")
skip_build = True if app_path is not None:
sys.argv.remove("--skip-build") if not os.path.isdir(app_path) or not os.path.isfile(os.path.join(app_path, "manifest.properties")):
if "--local-sdk" in sys.argv: exit_with_error(f"App path not found or missing manifest.properties: {app_path}")
use_local_sdk = True if verbose:
sys.argv.remove("--local-sdk") print_status_success(f"Switching to app directory: {app_path}")
os.chdir(app_path)
# 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]
# Environment setup # Environment setup
setup_environment() setup_environment()
@@ -755,58 +1008,30 @@ if __name__ == "__main__":
exit_with_error("manifest.properties not found") exit_with_error("manifest.properties not found")
manifest = read_manifest() manifest = read_manifest()
validate_manifest(manifest) validate_manifest(manifest)
all_platform_targets = manifest["target.platforms"].split(",")
# Update SDK cache (tool.json) # Update SDK cache (tool.json)
if not use_local_sdk and should_update_tool_json() and not update_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") exit_with_error("Failed to retrieve SDK info")
# Actions # Actions
action_arg = parsed_command.action
if action_arg == "build": if action_arg == "build":
if len(sys.argv) < 2: if not build_action(manifest, parsed_command.arguments):
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):
sys.exit(1) sys.exit(1)
elif action_arg == "clean": elif action_arg == "clean":
clean_action() clean_action(manifest)
elif action_arg == "clearcache": elif action_arg == "clearcache":
clear_cache_action() clear_cache_action()
elif action_arg == "updateself": elif action_arg == "updateself":
update_self_action() update_self_action()
elif action_arg == "run": elif action_arg == "run":
if len(sys.argv) < 3: run_action(manifest, parsed_command.arguments)
print_help()
exit_with_error("Commandline parameter missing")
run_action(manifest, sys.argv[2])
elif action_arg == "install": elif action_arg == "install":
if len(sys.argv) < 3: install_action(manifest, parsed_command.arguments)
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)
elif action_arg == "uninstall": elif action_arg == "uninstall":
if len(sys.argv) < 3: uninstall_action(manifest, parsed_command.arguments)
print_help()
exit_with_error("Commandline parameter missing")
uninstall_action(manifest, sys.argv[2])
elif action_arg == "bir" or action_arg == "brrr": elif action_arg == "bir" or action_arg == "brrr":
if len(sys.argv) < 3: if build_action(manifest, parsed_command.arguments):
print_help() if install_action(manifest, parsed_command.arguments):
exit_with_error("Commandline parameter missing") run_action(manifest, parsed_command.arguments)
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])
else: else:
print_help() print_help()
exit_with_error("Unknown commandline parameter") exit_with_error("Unknown commandline parameter")