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