This is an automated email from the ASF dual-hosted git repository.
acassis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx-apps.git
The following commit(s) were added to refs/heads/master by this push:
new 65854ea80 system/nxpkg: add local package lifecycle helper
65854ea80 is described below
commit 65854ea80563b91e11e9763ec2fa826b5a32657e
Author: aviralgarg05 <[email protected]>
AuthorDate: Mon May 18 15:56:33 2026 +0530
system/nxpkg: add local package lifecycle helper
Add the initial nxpkg command, metadata and store handling, the local
install/list path, and the repository export helper.
Keep the current flow scoped to local artifacts and target-qualified
repository entries so it remains usable as an incremental MVP while follow-up
features land separately.
Signed-off-by: aviralgarg05 <[email protected]>
---
system/nxpkg/CMakeLists.txt | 43 +++
system/nxpkg/Kconfig | 32 ++
system/nxpkg/Make.defs | 25 ++
system/nxpkg/Makefile | 34 ++
system/nxpkg/pkg.h | 187 +++++++++++
system/nxpkg/pkg_compat.c | 64 ++++
system/nxpkg/pkg_hash.c | 98 ++++++
system/nxpkg/pkg_install.c | 501 ++++++++++++++++++++++++++++
system/nxpkg/pkg_log.c | 65 ++++
system/nxpkg/pkg_main.c | 106 ++++++
system/nxpkg/pkg_manifest.c | 155 +++++++++
system/nxpkg/pkg_metadata.c | 770 ++++++++++++++++++++++++++++++++++++++++++++
system/nxpkg/pkg_store.c | 501 ++++++++++++++++++++++++++++
system/nxpkg/pkg_txn.c | 110 +++++++
tools/export_pkg_repo.py | 229 +++++++++++++
15 files changed, 2920 insertions(+)
diff --git a/system/nxpkg/CMakeLists.txt b/system/nxpkg/CMakeLists.txt
new file mode 100644
index 000000000..8ac3e3585
--- /dev/null
+++ b/system/nxpkg/CMakeLists.txt
@@ -0,0 +1,43 @@
+#
##############################################################################
+# apps/system/nxpkg/CMakeLists.txt
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
contributor
+# license agreements. See the NOTICE file distributed with this work for
+# additional information regarding copyright ownership. The ASF licenses this
+# file to you under the Apache License, Version 2.0 (the "License"); you may
not
+# use this file except in compliance with the License. You may obtain a copy
of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+#
+#
##############################################################################
+
+if(CONFIG_SYSTEM_NXPKG)
+ nuttx_add_application(
+ MODULE
+ ${CONFIG_SYSTEM_NXPKG}
+ NAME
+ ${CONFIG_SYSTEM_NXPKG_PROGNAME}
+ STACKSIZE
+ ${CONFIG_SYSTEM_NXPKG_STACKSIZE}
+ PRIORITY
+ ${CONFIG_SYSTEM_NXPKG_PRIORITY}
+ SRCS
+ pkg_main.c
+ pkg_compat.c
+ pkg_hash.c
+ pkg_install.c
+ pkg_log.c
+ pkg_manifest.c
+ pkg_metadata.c
+ pkg_store.c
+ pkg_txn.c)
+endif()
diff --git a/system/nxpkg/Kconfig b/system/nxpkg/Kconfig
new file mode 100644
index 000000000..afc7fa326
--- /dev/null
+++ b/system/nxpkg/Kconfig
@@ -0,0 +1,32 @@
+#
+# For a description of the syntax of this configuration file,
+# see the file kconfig-language.txt in the NuttX tools repository.
+#
+
+config SYSTEM_NXPKG
+ bool "'nxpkg' lifecycle helper"
+ default n
+ select CRYPTO
+ select NETUTILS_CJSON
+ ---help---
+ Enable the thin package lifecycle helper proposed for the
+ Dynamic ELF distribution work.
+
+if SYSTEM_NXPKG
+
+config SYSTEM_NXPKG_PROGNAME
+ string "Program name"
+ default "nxpkg"
+ ---help---
+ This is the name of the program that will be used when the
+ nxpkg tool is installed.
+
+config SYSTEM_NXPKG_PRIORITY
+ int "'nxpkg' task priority"
+ default 100
+
+config SYSTEM_NXPKG_STACKSIZE
+ int "'nxpkg' stack size"
+ default 16384
+
+endif
diff --git a/system/nxpkg/Make.defs b/system/nxpkg/Make.defs
new file mode 100644
index 000000000..bdb799080
--- /dev/null
+++ b/system/nxpkg/Make.defs
@@ -0,0 +1,25 @@
+############################################################################
+# apps/system/nxpkg/Make.defs
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership. The
+# ASF licenses this file to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+############################################################################
+
+ifneq ($(CONFIG_SYSTEM_NXPKG),)
+CONFIGURED_APPS += $(APPDIR)/system/nxpkg
+endif
diff --git a/system/nxpkg/Makefile b/system/nxpkg/Makefile
new file mode 100644
index 000000000..757f9fc89
--- /dev/null
+++ b/system/nxpkg/Makefile
@@ -0,0 +1,34 @@
+############################################################################
+# apps/system/nxpkg/Makefile
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership. The
+# ASF licenses this file to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+############################################################################
+
+include $(APPDIR)/Make.defs
+
+PROGNAME = $(CONFIG_SYSTEM_NXPKG_PROGNAME)
+PRIORITY = $(CONFIG_SYSTEM_NXPKG_PRIORITY)
+STACKSIZE = $(CONFIG_SYSTEM_NXPKG_STACKSIZE)
+MODULE = $(CONFIG_SYSTEM_NXPKG)
+
+CSRCS = pkg_compat.c pkg_hash.c pkg_install.c pkg_log.c pkg_manifest.c
+CSRCS += pkg_metadata.c pkg_store.c pkg_txn.c
+MAINSRC = pkg_main.c
+
+include $(APPDIR)/Application.mk
diff --git a/system/nxpkg/pkg.h b/system/nxpkg/pkg.h
new file mode 100644
index 000000000..39a4bf0c2
--- /dev/null
+++ b/system/nxpkg/pkg.h
@@ -0,0 +1,187 @@
+/****************************************************************************
+ * apps/system/nxpkg/pkg.h
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+#ifndef __APPS_SYSTEM_NXPKG_PKG_H
+#define __APPS_SYSTEM_NXPKG_PKG_H
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <limits.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+
+/****************************************************************************
+ * Pre-processor Definitions
+ ****************************************************************************/
+
+#define PKG_REPO_DIR "/etc/nxpkg"
+#define PKG_REPO_INDEX "/etc/nxpkg/index.json"
+#define PKG_REPO_INSTALLED "/var/lib/nxpkg/installed.json"
+#define PKG_STORE_DIR "/var/lib/nxpkg/pkgs"
+#define PKG_TMP_DIR "/var/cache/nxpkg"
+#define PKG_TMP_PKG_DIR "/var/cache/nxpkg/pkg"
+
+#define PKG_NAME_MAX 63
+#define PKG_VERSION_MAX 31
+#define PKG_ARCH_MAX 31
+#define PKG_COMPAT_MAX 63
+#define PKG_HASH_HEX_LEN 64
+#define PKG_INDEX_MAX 32
+#define PKG_INSTALLED_MAX 16
+#define PKG_INSTALLED_VERSIONS_MAX 8
+
+/****************************************************************************
+ * Public Types
+ ****************************************************************************/
+
+enum pkg_payload_type_e
+{
+ PKG_PAYLOAD_ELF = 0,
+ PKG_PAYLOAD_SHARED_LIB
+};
+
+enum pkg_txn_state_e
+{
+ PKG_TXN_IDLE = 0,
+ PKG_TXN_FETCHING,
+ PKG_TXN_VERIFIED,
+ PKG_TXN_STAGED,
+ PKG_TXN_COMPAT_OK,
+ PKG_TXN_ACTIVATED,
+ PKG_TXN_CLEANUP,
+ PKG_TXN_FAILED,
+ PKG_TXN_RESTORE
+};
+
+struct pkg_manifest_s
+{
+ char name[PKG_NAME_MAX + 1];
+ char version[PKG_VERSION_MAX + 1];
+ char arch[PKG_ARCH_MAX + 1];
+ char compat[PKG_COMPAT_MAX + 1];
+ char artifact[PATH_MAX];
+ char sha256[PKG_HASH_HEX_LEN + 1];
+ enum pkg_payload_type_e type;
+};
+
+struct pkg_index_s
+{
+ struct pkg_manifest_s manifests[PKG_INDEX_MAX];
+ size_t count;
+};
+
+struct pkg_installed_entry_s
+{
+ char name[PKG_NAME_MAX + 1];
+ char current[PKG_VERSION_MAX + 1];
+ char previous[PKG_VERSION_MAX + 1];
+ char arch[PKG_ARCH_MAX + 1];
+ char compat[PKG_COMPAT_MAX + 1];
+ char versions[PKG_INSTALLED_VERSIONS_MAX][PKG_VERSION_MAX + 1];
+ enum pkg_payload_type_e type;
+ size_t version_count;
+};
+
+struct pkg_installed_db_s
+{
+ struct pkg_installed_entry_s entries[PKG_INSTALLED_MAX];
+ size_t count;
+};
+
+/****************************************************************************
+ * Public Function Prototypes
+ ****************************************************************************/
+
+const char *pkg_manifest_type_str(enum pkg_payload_type_e type);
+int pkg_manifest_validate(FAR const struct pkg_manifest_s *manifest);
+int pkg_manifest_parse_type(FAR const char *value,
+ FAR enum pkg_payload_type_e *type);
+
+int pkg_store_prepare_layout(void);
+int pkg_store_ensure_package_root(FAR const char *name);
+int pkg_store_ensure_version_dir(FAR const char *name,
+ FAR const char *version);
+int pkg_store_format_index_path(FAR char *buffer, size_t size);
+int pkg_store_format_installed_path(FAR char *buffer, size_t size);
+int pkg_store_format_package_root(FAR char *buffer, size_t size,
+ FAR const char *name);
+int pkg_store_format_version_path(FAR char *buffer, size_t size,
+ FAR const char *name,
+ FAR const char *version);
+int pkg_store_format_current_path(FAR char *buffer, size_t size,
+ FAR const char *name);
+int pkg_store_format_previous_path(FAR char *buffer, size_t size,
+ FAR const char *name);
+int pkg_store_format_txn_path(FAR char *buffer, size_t size,
+ FAR const char *name);
+int pkg_store_format_lock_path(FAR char *buffer, size_t size,
+ FAR const char *name);
+int pkg_store_format_download_path(FAR char *buffer, size_t size,
+ FAR const char *name,
+ FAR const char *version);
+int pkg_store_format_payload_path(FAR char *buffer, size_t size,
+ FAR const char *name,
+ FAR const char *version,
+ FAR const char *artifact);
+int pkg_store_format_manifest_path(FAR char *buffer, size_t size,
+ FAR const char *name,
+ FAR const char *version);
+int pkg_store_read_text(FAR const char *path, FAR char **buffer);
+int pkg_store_write_text_atomic(FAR const char *path, FAR const char *text);
+int pkg_store_copy_file(FAR const char *src, FAR const char *dest);
+int pkg_store_remove_file(FAR const char *path);
+
+const char *pkg_runtime_arch(void);
+const char *pkg_runtime_compat(void);
+int pkg_compat_check(FAR const struct pkg_manifest_s *manifest);
+
+int pkg_hash_file_sha256(FAR const char *path,
+ FAR char digest[PKG_HASH_HEX_LEN + 1]);
+
+int pkg_metadata_load_index(FAR struct pkg_index_s *index);
+FAR const struct pkg_manifest_s *
+pkg_metadata_find_latest(FAR const struct pkg_index_s *index,
+ FAR const char *name);
+int pkg_metadata_load_installed(FAR struct pkg_installed_db_s *db);
+int pkg_metadata_save_installed(FAR const struct pkg_installed_db_s *db);
+FAR struct pkg_installed_entry_s *
+pkg_metadata_find_installed(FAR struct pkg_installed_db_s *db,
+ FAR const char *name);
+int pkg_metadata_write_manifest(FAR const char *path,
+ FAR const struct pkg_manifest_s *manifest);
+int pkg_metadata_print_installed(FAR FILE *stream,
+ FAR const struct pkg_installed_db_s *db);
+
+const char *pkg_txn_state_str(enum pkg_txn_state_e state);
+int pkg_txn_write_state(FAR const char *name, enum pkg_txn_state_e state);
+int pkg_txn_clear_state(FAR const char *name);
+
+int pkg_install(FAR const char *name);
+int pkg_list(FAR FILE *stream);
+
+void pkg_error(FAR const char *fmt, ...);
+void pkg_info(FAR const char *fmt, ...);
+
+#endif /* __APPS_SYSTEM_NXPKG_PKG_H */
diff --git a/system/nxpkg/pkg_compat.c b/system/nxpkg/pkg_compat.c
new file mode 100644
index 000000000..301fb07aa
--- /dev/null
+++ b/system/nxpkg/pkg_compat.c
@@ -0,0 +1,64 @@
+/****************************************************************************
+ * apps/system/nxpkg/pkg_compat.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <errno.h>
+#include <string.h>
+
+#include "pkg.h"
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+const char *pkg_runtime_arch(void)
+{
+ return CONFIG_ARCH;
+}
+
+const char *pkg_runtime_compat(void)
+{
+ return CONFIG_ARCH_BOARD;
+}
+
+int pkg_compat_check(FAR const struct pkg_manifest_s *manifest)
+{
+ if (manifest == NULL)
+ {
+ return -EINVAL;
+ }
+
+ if (strcmp(manifest->arch, pkg_runtime_arch()) != 0)
+ {
+ return -ENOEXEC;
+ }
+
+ if (strcmp(manifest->compat, pkg_runtime_compat()) != 0)
+ {
+ return -EXDEV;
+ }
+
+ return 0;
+}
diff --git a/system/nxpkg/pkg_hash.c b/system/nxpkg/pkg_hash.c
new file mode 100644
index 000000000..8ea1889f5
--- /dev/null
+++ b/system/nxpkg/pkg_hash.c
@@ -0,0 +1,98 @@
+/****************************************************************************
+ * apps/system/nxpkg/pkg_hash.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <crypto/sha2.h>
+#include <errno.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include "pkg.h"
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+static void pkg_hex_encode(FAR const uint8_t *input, size_t length,
+ FAR char *output)
+{
+ static const char g_hex[] = "0123456789abcdef";
+ size_t i;
+
+ for (i = 0; i < length; i++)
+ {
+ output[i * 2] = g_hex[input[i] >> 4];
+ output[i * 2 + 1] = g_hex[input[i] & 0x0f];
+ }
+
+ output[length * 2] = '\0';
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+int pkg_hash_file_sha256(FAR const char *path,
+ FAR char digest[PKG_HASH_HEX_LEN + 1])
+{
+ SHA2_CTX ctx;
+ FAR FILE *stream;
+ uint8_t raw[SHA256_DIGEST_LENGTH];
+ uint8_t buffer[512];
+ size_t nread;
+
+ stream = fopen(path, "rb");
+ if (stream == NULL)
+ {
+ return -errno;
+ }
+
+ sha256init(&ctx);
+
+ for (; ; )
+ {
+ nread = fread(buffer, 1, sizeof(buffer), stream);
+ if (nread > 0)
+ {
+ sha256update(&ctx, buffer, nread);
+ }
+
+ if (nread < sizeof(buffer))
+ {
+ if (ferror(stream))
+ {
+ fclose(stream);
+ return -EIO;
+ }
+
+ break;
+ }
+ }
+
+ fclose(stream);
+ sha256final(raw, &ctx);
+ pkg_hex_encode(raw, sizeof(raw), digest);
+ return 0;
+}
diff --git a/system/nxpkg/pkg_install.c b/system/nxpkg/pkg_install.c
new file mode 100644
index 000000000..c40982001
--- /dev/null
+++ b/system/nxpkg/pkg_install.c
@@ -0,0 +1,501 @@
+/****************************************************************************
+ * apps/system/nxpkg/pkg_install.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "pkg.h"
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+static int pkg_install_resolve_artifact(FAR char *buffer, size_t size,
+ FAR const struct pkg_manifest_s
+ *manifest)
+{
+ int ret;
+
+ if (manifest->artifact[0] == '/')
+ {
+ ret = snprintf(buffer, size, "%s", manifest->artifact);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ return (size_t)ret >= size ? -ENAMETOOLONG : 0;
+ }
+
+ ret = snprintf(buffer, size, "%s/%s", PKG_REPO_DIR, manifest->artifact);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ return (size_t)ret >= size ? -ENAMETOOLONG : 0;
+}
+
+static int pkg_install_acquire_lock(FAR const char *name, FAR char *path,
+ size_t size)
+{
+ int fd;
+ int ret;
+
+ ret = pkg_store_ensure_package_root(name);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = pkg_store_format_lock_path(path, size, name);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0644);
+ if (fd < 0)
+ {
+ return errno == EEXIST ? -EBUSY : -errno;
+ }
+
+ close(fd);
+ return 0;
+}
+
+static bool pkg_install_has_version(
+ FAR const struct pkg_installed_entry_s *entry,
+ FAR const char *version)
+{
+ size_t i;
+
+ for (i = 0; i < entry->version_count; i++)
+ {
+ if (strcmp(entry->versions[i], version) == 0)
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static int pkg_install_add_version(FAR struct pkg_installed_entry_s *entry,
+ FAR const char *version)
+{
+ int ret;
+
+ if (pkg_install_has_version(entry, version))
+ {
+ return 0;
+ }
+
+ if (entry->version_count >= PKG_INSTALLED_VERSIONS_MAX)
+ {
+ return -E2BIG;
+ }
+
+ ret = snprintf(entry->versions[entry->version_count],
+ sizeof(entry->versions[entry->version_count]),
+ "%s", version);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ if ((size_t)ret >= sizeof(entry->versions[entry->version_count]))
+ {
+ return -ENAMETOOLONG;
+ }
+
+ entry->version_count++;
+ return 0;
+}
+
+static int pkg_install_update_installed(FAR struct pkg_installed_db_s *db,
+ FAR const struct pkg_manifest_s
+ *manifest)
+{
+ FAR struct pkg_installed_entry_s *entry;
+ int ret;
+
+ entry = pkg_metadata_find_installed(db, manifest->name);
+ if (entry == NULL)
+ {
+ if (db->count >= PKG_INSTALLED_MAX)
+ {
+ return -E2BIG;
+ }
+
+ entry = &db->entries[db->count++];
+ memset(entry, 0, sizeof(*entry));
+ ret = snprintf(entry->name, sizeof(entry->name), "%s", manifest->name);
+ if (ret < 0 || (size_t)ret >= sizeof(entry->name))
+ {
+ return -ENAMETOOLONG;
+ }
+ }
+
+ if (entry->current[0] != '\0' &&
+ strcmp(entry->current, manifest->version) != 0)
+ {
+ ret = snprintf(entry->previous, sizeof(entry->previous), "%s",
+ entry->current);
+ if (ret < 0 || (size_t)ret >= sizeof(entry->previous))
+ {
+ return -ENAMETOOLONG;
+ }
+ }
+
+ ret = snprintf(entry->current, sizeof(entry->current), "%s",
+ manifest->version);
+ if (ret < 0 || (size_t)ret >= sizeof(entry->current))
+ {
+ return -ENAMETOOLONG;
+ }
+
+ ret = snprintf(entry->arch, sizeof(entry->arch), "%s", manifest->arch);
+ if (ret < 0 || (size_t)ret >= sizeof(entry->arch))
+ {
+ return -ENAMETOOLONG;
+ }
+
+ ret = snprintf(entry->compat, sizeof(entry->compat), "%s",
+ manifest->compat);
+ if (ret < 0 || (size_t)ret >= sizeof(entry->compat))
+ {
+ return -ENAMETOOLONG;
+ }
+
+ entry->type = manifest->type;
+ return pkg_install_add_version(entry, manifest->version);
+}
+
+static int pkg_install_write_pointers(
+ FAR const struct pkg_installed_db_s *db,
+ FAR const struct pkg_manifest_s *manifest)
+{
+ FAR struct pkg_installed_entry_s *entry;
+ char current[PATH_MAX];
+ char previous[PATH_MAX];
+ int ret;
+
+ entry = pkg_metadata_find_installed((FAR struct pkg_installed_db_s *)db,
+ manifest->name);
+ if (entry == NULL)
+ {
+ return -ENOENT;
+ }
+
+ ret = pkg_store_format_current_path(current, sizeof(current),
+ manifest->name);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = pkg_store_format_previous_path(previous, sizeof(previous),
+ manifest->name);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = pkg_store_write_text_atomic(current, entry->current);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ return pkg_store_write_text_atomic(previous, entry->previous);
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+int pkg_install(FAR const char *name)
+{
+ FAR struct pkg_index_s *index;
+ FAR struct pkg_installed_db_s *installed;
+ FAR const struct pkg_manifest_s *manifest;
+ char source[PATH_MAX];
+ char tmp[PATH_MAX] = "";
+ char payload[PATH_MAX];
+ char manifest_path[PATH_MAX];
+ char lock[PATH_MAX] = "";
+ char digest[PKG_HASH_HEX_LEN + 1];
+ int ret;
+
+ index = malloc(sizeof(*index));
+ installed = malloc(sizeof(*installed));
+ if (index == NULL || installed == NULL)
+ {
+ free(index);
+ free(installed);
+ pkg_error("unable to allocate package metadata buffers");
+ return EXIT_FAILURE;
+ }
+
+ ret = pkg_store_prepare_layout();
+ if (ret < 0)
+ {
+ free(index);
+ free(installed);
+ pkg_error("unable to prepare package layout: %d", ret);
+ return EXIT_FAILURE;
+ }
+
+ ret = pkg_metadata_load_index(index);
+ if (ret < 0)
+ {
+ free(index);
+ free(installed);
+ pkg_error("unable to load local index metadata: %d", ret);
+ return EXIT_FAILURE;
+ }
+
+ manifest = pkg_metadata_find_latest(index, name);
+ if (manifest == NULL)
+ {
+ free(index);
+ free(installed);
+ pkg_error("package '%s' not found in local index", name);
+ return EXIT_FAILURE;
+ }
+
+ ret = pkg_install_resolve_artifact(source, sizeof(source), manifest);
+ if (ret < 0)
+ {
+ pkg_error("artifact path for '%s' is too long", name);
+ return EXIT_FAILURE;
+ }
+
+ ret = pkg_install_acquire_lock(name, lock, sizeof(lock));
+ if (ret < 0)
+ {
+ pkg_error("unable to acquire package lock for '%s': %d", name, ret);
+ return EXIT_FAILURE;
+ }
+
+ ret = pkg_txn_write_state(name, PKG_TXN_FETCHING);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_store_format_download_path(tmp, sizeof(tmp), manifest->name,
+ manifest->version);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_store_copy_file(source, tmp);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_hash_file_sha256(tmp, digest);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ if (strcasecmp(digest, manifest->sha256) != 0)
+ {
+ ret = -EILSEQ;
+ goto errout;
+ }
+
+ ret = pkg_txn_write_state(name, PKG_TXN_VERIFIED);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_store_ensure_version_dir(manifest->name, manifest->version);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_store_format_payload_path(payload, sizeof(payload),
+ manifest->name, manifest->version,
+ manifest->artifact);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_store_copy_file(tmp, payload);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_store_format_manifest_path(manifest_path, sizeof(manifest_path),
+ manifest->name, manifest->version);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_metadata_write_manifest(manifest_path, manifest);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_txn_write_state(name, PKG_TXN_STAGED);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_compat_check(manifest);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_txn_write_state(name, PKG_TXN_COMPAT_OK);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_metadata_load_installed(installed);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_install_update_installed(installed, manifest);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_install_write_pointers(installed, manifest);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_metadata_save_installed(installed);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ ret = pkg_txn_write_state(name, PKG_TXN_ACTIVATED);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+
+ pkg_txn_write_state(name, PKG_TXN_CLEANUP);
+ if (tmp[0] != '\0')
+ {
+ pkg_store_remove_file(tmp);
+ }
+
+ pkg_txn_clear_state(name);
+ if (lock[0] != '\0')
+ {
+ pkg_store_remove_file(lock);
+ }
+
+ pkg_info("installed %s version %s", manifest->name, manifest->version);
+ free(index);
+ free(installed);
+ return EXIT_SUCCESS;
+
+errout:
+ pkg_txn_write_state(name, PKG_TXN_FAILED);
+ if (tmp[0] != '\0')
+ {
+ pkg_store_remove_file(tmp);
+ }
+
+ pkg_txn_clear_state(name);
+ if (lock[0] != '\0')
+ {
+ pkg_store_remove_file(lock);
+ }
+
+ free(index);
+ free(installed);
+ pkg_error("install failed for '%s': %d", name, ret);
+ return EXIT_FAILURE;
+}
+
+int pkg_list(FAR FILE *stream)
+{
+ FAR struct pkg_installed_db_s *db;
+ int ret;
+
+ db = malloc(sizeof(*db));
+ if (db == NULL)
+ {
+ pkg_error("unable to allocate installed metadata buffer");
+ return EXIT_FAILURE;
+ }
+
+ ret = pkg_store_prepare_layout();
+ if (ret < 0)
+ {
+ free(db);
+ pkg_error("unable to prepare package layout: %d", ret);
+ return EXIT_FAILURE;
+ }
+
+ ret = pkg_metadata_load_installed(db);
+ if (ret < 0)
+ {
+ free(db);
+ pkg_error("unable to load installed metadata: %d", ret);
+ return EXIT_FAILURE;
+ }
+
+ ret = pkg_metadata_print_installed(stream, db);
+ if (ret < 0)
+ {
+ free(db);
+ pkg_error("unable to print installed metadata: %d", ret);
+ return EXIT_FAILURE;
+ }
+
+ free(db);
+ return EXIT_SUCCESS;
+}
diff --git a/system/nxpkg/pkg_log.c b/system/nxpkg/pkg_log.c
new file mode 100644
index 000000000..bed06b13f
--- /dev/null
+++ b/system/nxpkg/pkg_log.c
@@ -0,0 +1,65 @@
+/****************************************************************************
+ * apps/system/nxpkg/pkg_log.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <stdarg.h>
+#include <stdio.h>
+
+#include "pkg.h"
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+static void pkg_vlog(FAR FILE *stream, FAR const char *level,
+ FAR const char *fmt, va_list ap)
+{
+ fprintf(stream, "nxpkg: %s: ", level);
+ vfprintf(stream, fmt, ap);
+ fputc('\n', stream);
+ fflush(stream);
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+void pkg_error(FAR const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ pkg_vlog(stderr, "error", fmt, ap);
+ va_end(ap);
+}
+
+void pkg_info(FAR const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ pkg_vlog(stdout, "info", fmt, ap);
+ va_end(ap);
+}
diff --git a/system/nxpkg/pkg_main.c b/system/nxpkg/pkg_main.c
new file mode 100644
index 000000000..c049592c5
--- /dev/null
+++ b/system/nxpkg/pkg_main.c
@@ -0,0 +1,106 @@
+/****************************************************************************
+ * apps/system/nxpkg/pkg_main.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "pkg.h"
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+int main(int argc, FAR char *argv[])
+{
+ FAR const char *cmd;
+
+ if (argc < 2)
+ {
+ fprintf(stderr,
+ "Usage: %s <install|update|list|rollback|help> [args]\n",
+ argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ cmd = argv[1];
+
+ if (strcmp(cmd, "help") == 0 || strcmp(cmd, "--help") == 0 ||
+ strcmp(cmd, "-h") == 0)
+ {
+ fprintf(stdout,
+ "Usage: %s <install|update|list|rollback|help> [args]\n",
+ argv[0]);
+ return EXIT_SUCCESS;
+ }
+
+ if (strcmp(cmd, "install") == 0)
+ {
+ if (argc != 3)
+ {
+ pkg_error("install expects exactly one package name");
+ fprintf(stderr,
+ "Usage: %s <install|update|list|rollback|help> [args]\n",
+ argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ return pkg_install(argv[2]);
+ }
+
+ if (strcmp(cmd, "update") == 0)
+ {
+ pkg_error("'update' is not implemented yet in the current unit");
+ return EXIT_FAILURE;
+ }
+
+ if (strcmp(cmd, "list") == 0)
+ {
+ if (argc != 2)
+ {
+ pkg_error("list does not take additional arguments");
+ fprintf(stderr,
+ "Usage: %s <install|update|list|rollback|help> [args]\n",
+ argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ return pkg_list(stdout);
+ }
+
+ if (strcmp(cmd, "rollback") == 0)
+ {
+ pkg_error("'rollback' is not implemented yet in the current unit");
+ return EXIT_FAILURE;
+ }
+
+ fprintf(stderr, "ERROR: Unknown subcommand '%s'\n", cmd);
+ fprintf(stderr,
+ "Usage: %s <install|update|list|rollback|help> [args]\n",
+ argv[0]);
+ return EXIT_FAILURE;
+}
diff --git a/system/nxpkg/pkg_manifest.c b/system/nxpkg/pkg_manifest.c
new file mode 100644
index 000000000..3e7b56a26
--- /dev/null
+++ b/system/nxpkg/pkg_manifest.c
@@ -0,0 +1,155 @@
+/****************************************************************************
+ * apps/system/nxpkg/pkg_manifest.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <ctype.h>
+#include <errno.h>
+#include <string.h>
+
+#include "pkg.h"
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+static bool pkg_has_nonspace(FAR const char *value)
+{
+ while (*value != '\0')
+ {
+ if (!isspace((unsigned char)*value))
+ {
+ return true;
+ }
+
+ value++;
+ }
+
+ return false;
+}
+
+static int pkg_validate_required(FAR const char *value)
+{
+ if (value == NULL || value[0] == '\0' || !pkg_has_nonspace(value))
+ {
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static bool pkg_validate_hex(FAR const char *value)
+{
+ while (*value != '\0')
+ {
+ if (!isxdigit((unsigned char)*value))
+ {
+ return false;
+ }
+
+ value++;
+ }
+
+ return true;
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+const char *pkg_manifest_type_str(enum pkg_payload_type_e type)
+{
+ switch (type)
+ {
+ case PKG_PAYLOAD_ELF:
+ return "elf";
+
+ case PKG_PAYLOAD_SHARED_LIB:
+ return "shared-lib";
+
+ default:
+ return "unknown";
+ }
+}
+
+int pkg_manifest_validate(FAR const struct pkg_manifest_s *manifest)
+{
+ if (manifest == NULL)
+ {
+ return -EINVAL;
+ }
+
+ if (pkg_validate_required(manifest->name) < 0 ||
+ pkg_validate_required(manifest->version) < 0 ||
+ pkg_validate_required(manifest->arch) < 0 ||
+ pkg_validate_required(manifest->compat) < 0 ||
+ pkg_validate_required(manifest->artifact) < 0 ||
+ pkg_validate_required(manifest->sha256) < 0)
+ {
+ return -EINVAL;
+ }
+
+ if (strlen(manifest->sha256) != PKG_HASH_HEX_LEN)
+ {
+ return -EINVAL;
+ }
+
+ if (!pkg_validate_hex(manifest->sha256))
+ {
+ return -EINVAL;
+ }
+
+ if (manifest->type != PKG_PAYLOAD_ELF &&
+ manifest->type != PKG_PAYLOAD_SHARED_LIB)
+ {
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+int pkg_manifest_parse_type(FAR const char *value,
+ FAR enum pkg_payload_type_e *type)
+{
+ if (value == NULL || type == NULL)
+ {
+ return -EINVAL;
+ }
+
+ if (strcmp(value, "elf") == 0)
+ {
+ *type = PKG_PAYLOAD_ELF;
+ return 0;
+ }
+
+ if (strcmp(value, "shared-lib") == 0 ||
+ strcmp(value, "shared_lib") == 0 ||
+ strcmp(value, "shared") == 0)
+ {
+ *type = PKG_PAYLOAD_SHARED_LIB;
+ return 0;
+ }
+
+ return -EINVAL;
+}
diff --git a/system/nxpkg/pkg_metadata.c b/system/nxpkg/pkg_metadata.c
new file mode 100644
index 000000000..aadb4692e
--- /dev/null
+++ b/system/nxpkg/pkg_metadata.c
@@ -0,0 +1,770 @@
+/****************************************************************************
+ * apps/system/nxpkg/pkg_metadata.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "netutils/cJSON.h"
+
+#include "pkg.h"
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+static int pkg_copy_string(FAR char *dest, size_t size, FAR const char *src)
+{
+ int ret;
+
+ ret = snprintf(dest, size, "%s", src);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ if ((size_t)ret >= size)
+ {
+ return -ENAMETOOLONG;
+ }
+
+ return 0;
+}
+
+static bool pkg_string_equal(FAR const char *lhs, size_t lhs_size,
+ FAR const char *rhs, size_t rhs_size)
+{
+ size_t lhs_len;
+ size_t rhs_len;
+
+ lhs_len = strnlen(lhs, lhs_size);
+ rhs_len = strnlen(rhs, rhs_size);
+
+ if (lhs_len >= lhs_size || rhs_len >= rhs_size)
+ {
+ return false;
+ }
+
+ return lhs_len == rhs_len && strncmp(lhs, rhs, lhs_len) == 0;
+}
+
+static int pkg_string_cmp(FAR const char *lhs, size_t lhs_size,
+ FAR const char *rhs, size_t rhs_size)
+{
+ size_t lhs_len;
+ size_t rhs_len;
+ size_t cmp_len;
+
+ lhs_len = strnlen(lhs, lhs_size);
+ rhs_len = strnlen(rhs, rhs_size);
+ cmp_len = lhs_len > rhs_len ? lhs_len : rhs_len;
+
+ if (lhs_len >= lhs_size || rhs_len >= rhs_size)
+ {
+ return strncmp(lhs, rhs, cmp_len);
+ }
+
+ return strncmp(lhs, rhs, cmp_len + 1);
+}
+
+static FAR cJSON *pkg_metadata_packages_array(FAR cJSON *root)
+{
+ if (cJSON_IsArray(root))
+ {
+ return root;
+ }
+
+ return cJSON_GetObjectItemCaseSensitive(root, "packages");
+}
+
+static int pkg_metadata_parse_manifest(FAR cJSON *item,
+ FAR struct pkg_manifest_s *manifest)
+{
+ FAR cJSON *field;
+ FAR const char *value;
+ int ret;
+
+ memset(manifest, 0, sizeof(*manifest));
+
+ field = cJSON_GetObjectItemCaseSensitive(item, "name");
+ value = cJSON_GetStringValue(field);
+ if (value == NULL ||
+ (ret = pkg_copy_string(manifest->name, sizeof(manifest->name),
+ value)) < 0)
+ {
+ return -EINVAL;
+ }
+
+ field = cJSON_GetObjectItemCaseSensitive(item, "version");
+ value = cJSON_GetStringValue(field);
+ if (value == NULL || (ret = pkg_copy_string(manifest->version,
+ sizeof(manifest->version),
+ value)) < 0)
+ {
+ return -EINVAL;
+ }
+
+ field = cJSON_GetObjectItemCaseSensitive(item, "arch");
+ value = cJSON_GetStringValue(field);
+ if (value == NULL ||
+ (ret = pkg_copy_string(manifest->arch, sizeof(manifest->arch),
+ value)) < 0)
+ {
+ return -EINVAL;
+ }
+
+ field = cJSON_GetObjectItemCaseSensitive(item, "compat");
+ value = cJSON_GetStringValue(field);
+ if (value == NULL || (ret = pkg_copy_string(manifest->compat,
+ sizeof(manifest->compat),
+ value)) < 0)
+ {
+ return -EINVAL;
+ }
+
+ field = cJSON_GetObjectItemCaseSensitive(item, "artifact");
+ value = cJSON_GetStringValue(field);
+ if (value == NULL || (ret = pkg_copy_string(manifest->artifact,
+ sizeof(manifest->artifact),
+ value)) < 0)
+ {
+ return -EINVAL;
+ }
+
+ field = cJSON_GetObjectItemCaseSensitive(item, "sha256");
+ value = cJSON_GetStringValue(field);
+ if (value == NULL ||
+ (ret = pkg_copy_string(manifest->sha256, sizeof(manifest->sha256),
+ value)) < 0)
+ {
+ return -EINVAL;
+ }
+
+ field = cJSON_GetObjectItemCaseSensitive(item, "type");
+ value = cJSON_GetStringValue(field);
+ if (pkg_manifest_parse_type(value, &manifest->type) < 0)
+ {
+ return -EINVAL;
+ }
+
+ return pkg_manifest_validate(manifest);
+}
+
+static int pkg_metadata_parse_versions(FAR cJSON *versions,
+ FAR struct pkg_installed_entry_s
+ *entry)
+{
+ FAR cJSON *item;
+ size_t count = 0;
+
+ if (!cJSON_IsArray(versions))
+ {
+ return -EINVAL;
+ }
+
+ cJSON_ArrayForEach(item, versions)
+ {
+ FAR const char *value;
+ int ret;
+
+ if (count >= PKG_INSTALLED_VERSIONS_MAX)
+ {
+ return -E2BIG;
+ }
+
+ value = cJSON_GetStringValue(item);
+ if (value == NULL)
+ {
+ return -EINVAL;
+ }
+
+ ret = pkg_copy_string(entry->versions[count],
+ sizeof(entry->versions[count]), value);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ count++;
+ }
+
+ entry->version_count = count;
+ return 0;
+}
+
+static int pkg_metadata_parse_installed_entry(
+ FAR cJSON *item,
+ FAR struct pkg_installed_entry_s *entry)
+{
+ FAR cJSON *field;
+ FAR const char *value;
+ int ret;
+
+ memset(entry, 0, sizeof(*entry));
+
+ field = cJSON_GetObjectItemCaseSensitive(item, "name");
+ value = cJSON_GetStringValue(field);
+ if (value == NULL ||
+ (ret = pkg_copy_string(entry->name, sizeof(entry->name), value)) < 0)
+ {
+ return -EINVAL;
+ }
+
+ field = cJSON_GetObjectItemCaseSensitive(item, "current");
+ value = cJSON_GetStringValue(field);
+ if (value == NULL || (ret = pkg_copy_string(entry->current,
+ sizeof(entry->current),
+ value)) < 0)
+ {
+ return -EINVAL;
+ }
+
+ field = cJSON_GetObjectItemCaseSensitive(item, "previous");
+ value = cJSON_GetStringValue(field);
+ if (value != NULL)
+ {
+ ret = pkg_copy_string(entry->previous, sizeof(entry->previous),
+ value);
+ if (ret < 0)
+ {
+ return ret;
+ }
+ }
+
+ field = cJSON_GetObjectItemCaseSensitive(item, "arch");
+ value = cJSON_GetStringValue(field);
+ if (value == NULL ||
+ (ret = pkg_copy_string(entry->arch, sizeof(entry->arch), value)) < 0)
+ {
+ return -EINVAL;
+ }
+
+ field = cJSON_GetObjectItemCaseSensitive(item, "compat");
+ value = cJSON_GetStringValue(field);
+ if (value == NULL ||
+ (ret = pkg_copy_string(entry->compat, sizeof(entry->compat),
+ value)) < 0)
+ {
+ return -EINVAL;
+ }
+
+ field = cJSON_GetObjectItemCaseSensitive(item, "type");
+ value = cJSON_GetStringValue(field);
+ if (pkg_manifest_parse_type(value, &entry->type) < 0)
+ {
+ return -EINVAL;
+ }
+
+ field = cJSON_GetObjectItemCaseSensitive(item, "versions");
+ ret = pkg_metadata_parse_versions(field, entry);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ return 0;
+}
+
+static int pkg_metadata_version_token_cmp(FAR const char *lhs,
+ FAR const char *rhs)
+{
+ long leftnum;
+ long rightnum;
+ FAR char *leftend;
+ FAR char *rightend;
+
+ leftnum = strtol(lhs, &leftend, 10);
+ rightnum = strtol(rhs, &rightend, 10);
+
+ if (leftend != lhs && rightend != rhs)
+ {
+ if (leftnum < rightnum)
+ {
+ return -1;
+ }
+
+ if (leftnum > rightnum)
+ {
+ return 1;
+ }
+ }
+ else
+ {
+ int ret;
+
+ ret = pkg_string_cmp(lhs, PKG_VERSION_MAX + 1,
+ rhs, PKG_VERSION_MAX + 1);
+ if (ret < 0)
+ {
+ return -1;
+ }
+
+ if (ret > 0)
+ {
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static int pkg_metadata_version_cmp(FAR const char *lhs, FAR const char *rhs)
+{
+ while (*lhs != '\0' || *rhs != '\0')
+ {
+ char leftbuf[16];
+ char rightbuf[16];
+ size_t leftlen = 0;
+ size_t rightlen = 0;
+ int ret;
+
+ while (lhs[leftlen] != '\0' && lhs[leftlen] != '.')
+ {
+ if (leftlen + 1 >= sizeof(leftbuf))
+ {
+ return pkg_string_cmp(lhs, PKG_VERSION_MAX + 1,
+ rhs, PKG_VERSION_MAX + 1);
+ }
+
+ leftbuf[leftlen] = lhs[leftlen];
+ leftlen++;
+ }
+
+ while (rhs[rightlen] != '\0' && rhs[rightlen] != '.')
+ {
+ if (rightlen + 1 >= sizeof(rightbuf))
+ {
+ return pkg_string_cmp(lhs, PKG_VERSION_MAX + 1,
+ rhs, PKG_VERSION_MAX + 1);
+ }
+
+ rightbuf[rightlen] = rhs[rightlen];
+ rightlen++;
+ }
+
+ leftbuf[leftlen] = '\0';
+ rightbuf[rightlen] = '\0';
+
+ ret = pkg_metadata_version_token_cmp(leftbuf, rightbuf);
+ if (ret != 0)
+ {
+ return ret;
+ }
+
+ lhs += leftlen;
+ rhs += rightlen;
+
+ if (*lhs == '.')
+ {
+ lhs++;
+ }
+
+ if (*rhs == '.')
+ {
+ rhs++;
+ }
+ }
+
+ return 0;
+}
+
+static FAR cJSON *pkg_metadata_manifest_to_json(
+ FAR const struct pkg_manifest_s *manifest)
+{
+ FAR cJSON *root;
+
+ root = cJSON_CreateObject();
+ if (root == NULL)
+ {
+ return NULL;
+ }
+
+ cJSON_AddStringToObject(root, "name", manifest->name);
+ cJSON_AddStringToObject(root, "version", manifest->version);
+ cJSON_AddStringToObject(root, "arch", manifest->arch);
+ cJSON_AddStringToObject(root, "compat", manifest->compat);
+ cJSON_AddStringToObject(root, "artifact", manifest->artifact);
+ cJSON_AddStringToObject(root, "sha256", manifest->sha256);
+ cJSON_AddStringToObject(root, "type",
+ pkg_manifest_type_str(manifest->type));
+ return root;
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+int pkg_metadata_load_index(FAR struct pkg_index_s *index)
+{
+ FAR cJSON *root;
+ FAR cJSON *packages;
+ FAR cJSON *item;
+ FAR char *text;
+ char path[PATH_MAX];
+ size_t count = 0;
+ size_t textlen;
+ int ret;
+
+ if (index == NULL)
+ {
+ return -EINVAL;
+ }
+
+ memset(index, 0, sizeof(*index));
+
+ ret = pkg_store_format_index_path(path, sizeof(path));
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ pkg_info("loading index from %s", path);
+
+ ret = pkg_store_read_text(path, &text);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ textlen = strlen(text);
+ pkg_info("index read complete (%zu bytes)", textlen);
+
+ root = cJSON_Parse(text);
+ pkg_info("cJSON_Parse returned %s", root != NULL ? "success" : "failure");
+ free(text);
+ if (root == NULL)
+ {
+ return -EINVAL;
+ }
+
+ packages = pkg_metadata_packages_array(root);
+ if (!cJSON_IsArray(packages))
+ {
+ cJSON_Delete(root);
+ return -EINVAL;
+ }
+
+ cJSON_ArrayForEach(item, packages)
+ {
+ if (count >= PKG_INDEX_MAX)
+ {
+ cJSON_Delete(root);
+ return -E2BIG;
+ }
+
+ ret = pkg_metadata_parse_manifest(item, &index->manifests[count]);
+ if (ret < 0)
+ {
+ cJSON_Delete(root);
+ return ret;
+ }
+
+ pkg_info("parsed manifest %s %s",
+ index->manifests[count].name,
+ index->manifests[count].version);
+ count++;
+ }
+
+ index->count = count;
+ cJSON_Delete(root);
+ return 0;
+}
+
+FAR const struct pkg_manifest_s *
+pkg_metadata_find_latest(FAR const struct pkg_index_s *index,
+ FAR const char *name)
+{
+ FAR const struct pkg_manifest_s *best = NULL;
+ FAR const char *arch;
+ FAR const char *compat;
+ size_t i;
+
+ if (index == NULL || name == NULL)
+ {
+ return NULL;
+ }
+
+ arch = pkg_runtime_arch();
+ compat = pkg_runtime_compat();
+
+ for (i = 0; i < index->count; i++)
+ {
+ FAR const struct pkg_manifest_s *candidate = &index->manifests[i];
+
+ if (!pkg_string_equal(candidate->name, sizeof(candidate->name),
+ name, PKG_NAME_MAX + 1))
+ {
+ continue;
+ }
+
+ if (!pkg_string_equal(candidate->arch, sizeof(candidate->arch),
+ arch, PKG_ARCH_MAX + 1) ||
+ !pkg_string_equal(candidate->compat, sizeof(candidate->compat),
+ compat, PKG_COMPAT_MAX + 1))
+ {
+ continue;
+ }
+
+ if (best == NULL ||
+ pkg_metadata_version_cmp(candidate->version, best->version) > 0)
+ {
+ best = candidate;
+ }
+ }
+
+ return best;
+}
+
+int pkg_metadata_load_installed(FAR struct pkg_installed_db_s *db)
+{
+ FAR cJSON *root;
+ FAR cJSON *packages;
+ FAR cJSON *item;
+ FAR char *text;
+ char path[PATH_MAX];
+ size_t count = 0;
+ int ret;
+
+ if (db == NULL)
+ {
+ return -EINVAL;
+ }
+
+ memset(db, 0, sizeof(*db));
+
+ ret = pkg_store_format_installed_path(path, sizeof(path));
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = pkg_store_read_text(path, &text);
+ if (ret == -ENOENT)
+ {
+ return 0;
+ }
+
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ root = cJSON_Parse(text);
+ free(text);
+ if (root == NULL)
+ {
+ return -EINVAL;
+ }
+
+ packages = pkg_metadata_packages_array(root);
+ if (!cJSON_IsArray(packages))
+ {
+ cJSON_Delete(root);
+ return -EINVAL;
+ }
+
+ cJSON_ArrayForEach(item, packages)
+ {
+ if (count >= PKG_INSTALLED_MAX)
+ {
+ cJSON_Delete(root);
+ return -E2BIG;
+ }
+
+ ret = pkg_metadata_parse_installed_entry(item, &db->entries[count]);
+ if (ret < 0)
+ {
+ cJSON_Delete(root);
+ return ret;
+ }
+
+ count++;
+ }
+
+ db->count = count;
+ cJSON_Delete(root);
+ return 0;
+}
+
+int pkg_metadata_save_installed(FAR const struct pkg_installed_db_s *db)
+{
+ FAR cJSON *root;
+ FAR cJSON *packages;
+ FAR char *text;
+ char path[PATH_MAX];
+ size_t i;
+ int ret;
+
+ if (db == NULL)
+ {
+ return -EINVAL;
+ }
+
+ root = cJSON_CreateObject();
+ if (root == NULL)
+ {
+ return -ENOMEM;
+ }
+
+ packages = cJSON_AddArrayToObject(root, "packages");
+ if (packages == NULL)
+ {
+ cJSON_Delete(root);
+ return -ENOMEM;
+ }
+
+ for (i = 0; i < db->count; i++)
+ {
+ FAR const struct pkg_installed_entry_s *entry = &db->entries[i];
+ FAR cJSON *item = cJSON_CreateObject();
+ FAR cJSON *versions = cJSON_AddArrayToObject(item, "versions");
+ size_t j;
+
+ if (item == NULL || versions == NULL)
+ {
+ cJSON_Delete(root);
+ return -ENOMEM;
+ }
+
+ cJSON_AddStringToObject(item, "name", entry->name);
+ cJSON_AddStringToObject(item, "current", entry->current);
+ cJSON_AddStringToObject(item, "previous", entry->previous);
+ cJSON_AddStringToObject(item, "arch", entry->arch);
+ cJSON_AddStringToObject(item, "compat", entry->compat);
+ cJSON_AddStringToObject(item, "type",
+ pkg_manifest_type_str(entry->type));
+
+ for (j = 0; j < entry->version_count; j++)
+ {
+ cJSON_AddItemToArray(versions,
+ cJSON_CreateString(entry->versions[j]));
+ }
+
+ cJSON_AddItemToArray(packages, item);
+ }
+
+ text = cJSON_PrintUnformatted(root);
+ cJSON_Delete(root);
+ if (text == NULL)
+ {
+ return -ENOMEM;
+ }
+
+ ret = pkg_store_format_installed_path(path, sizeof(path));
+ if (ret < 0)
+ {
+ cJSON_free(text);
+ return ret;
+ }
+
+ ret = pkg_store_write_text_atomic(path, text);
+ cJSON_free(text);
+ return ret;
+}
+
+FAR struct pkg_installed_entry_s *
+pkg_metadata_find_installed(FAR struct pkg_installed_db_s *db,
+ FAR const char *name)
+{
+ size_t i;
+
+ if (db == NULL || name == NULL)
+ {
+ return NULL;
+ }
+
+ for (i = 0; i < db->count; i++)
+ {
+ if (pkg_string_equal(db->entries[i].name, sizeof(db->entries[i].name),
+ name, PKG_NAME_MAX + 1))
+ {
+ return &db->entries[i];
+ }
+ }
+
+ return NULL;
+}
+
+int pkg_metadata_write_manifest(FAR const char *path,
+ FAR const struct pkg_manifest_s *manifest)
+{
+ FAR cJSON *root;
+ FAR char *text;
+ int ret;
+
+ root = pkg_metadata_manifest_to_json(manifest);
+ if (root == NULL)
+ {
+ return -ENOMEM;
+ }
+
+ text = cJSON_PrintUnformatted(root);
+ cJSON_Delete(root);
+ if (text == NULL)
+ {
+ return -ENOMEM;
+ }
+
+ ret = pkg_store_write_text_atomic(path, text);
+ cJSON_free(text);
+ return ret;
+}
+
+int pkg_metadata_print_installed(FAR FILE *stream,
+ FAR const struct pkg_installed_db_s *db)
+{
+ size_t i;
+
+ if (stream == NULL || db == NULL)
+ {
+ return -EINVAL;
+ }
+
+ for (i = 0; i < db->count; i++)
+ {
+ FAR const struct pkg_installed_entry_s *entry = &db->entries[i];
+ size_t j;
+
+ fprintf(
+ stream,
+ "%s current=%s previous=%s type=%s arch=%s compat=%s versions=",
+ entry->name,
+ entry->current[0] != '\0' ? entry->current : "-",
+ entry->previous[0] != '\0' ? entry->previous : "-",
+ pkg_manifest_type_str(entry->type),
+ entry->arch,
+ entry->compat);
+
+ for (j = 0; j < entry->version_count; j++)
+ {
+ fprintf(stream, "%s%s", j == 0 ? "" : ",", entry->versions[j]);
+ }
+
+ fputc('\n', stream);
+ }
+
+ return 0;
+}
diff --git a/system/nxpkg/pkg_store.c b/system/nxpkg/pkg_store.c
new file mode 100644
index 000000000..0b23b054c
--- /dev/null
+++ b/system/nxpkg/pkg_store.c
@@ -0,0 +1,501 @@
+/****************************************************************************
+ * apps/system/nxpkg/pkg_store.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "pkg.h"
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+static int pkg_store_format(FAR char *buffer, size_t size,
+ FAR const char *fmt,
+ FAR const char *name,
+ FAR const char *version)
+{
+ int ret;
+
+ ret = snprintf(buffer, size, fmt, name, version);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ if ((size_t)ret >= size)
+ {
+ return -ENAMETOOLONG;
+ }
+
+ return 0;
+}
+
+static int pkg_store_mkdir(FAR const char *path)
+{
+ struct stat st;
+ int ret;
+
+ ret = stat(path, &st);
+ if (ret == 0)
+ {
+ if (!S_ISDIR(st.st_mode))
+ {
+ return -ENOTDIR;
+ }
+
+ return 0;
+ }
+
+ if (errno != ENOENT)
+ {
+ return -errno;
+ }
+
+ ret = mkdir(path, 0755);
+ if (ret < 0 && errno != EEXIST)
+ {
+ return -errno;
+ }
+
+ return 0;
+}
+
+static int pkg_store_mkdirs(FAR const char *path)
+{
+ char buffer[PATH_MAX];
+ FAR char *cursor;
+ int ret;
+
+ ret = snprintf(buffer, sizeof(buffer), "%s", path);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ if ((size_t)ret >= sizeof(buffer))
+ {
+ return -ENAMETOOLONG;
+ }
+
+ for (cursor = buffer + 1; *cursor != '\0'; cursor++)
+ {
+ if (*cursor != '/')
+ {
+ continue;
+ }
+
+ *cursor = '\0';
+ ret = pkg_store_mkdir(buffer);
+ *cursor = '/';
+ if (ret < 0)
+ {
+ return ret;
+ }
+ }
+
+ return pkg_store_mkdir(buffer);
+}
+
+static int pkg_store_write_all(int fd, FAR const char *buffer, size_t length)
+{
+ size_t offset = 0;
+
+ while (offset < length)
+ {
+ ssize_t ret;
+
+ ret = write(fd, buffer + offset, length - offset);
+ if (ret < 0)
+ {
+ if (errno == EINTR)
+ {
+ continue;
+ }
+
+ return -errno;
+ }
+
+ offset += (size_t)ret;
+ }
+
+ return 0;
+}
+
+static FAR const char *pkg_store_basename(FAR const char *path)
+{
+ FAR const char *leaf;
+
+ leaf = strrchr(path, '/');
+ return leaf != NULL ? leaf + 1 : path;
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+int pkg_store_prepare_layout(void)
+{
+ int ret;
+
+ ret = pkg_store_mkdirs(PKG_REPO_DIR);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = pkg_store_mkdirs(PKG_STORE_DIR);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = pkg_store_mkdirs(PKG_TMP_DIR);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ return pkg_store_mkdirs(PKG_TMP_PKG_DIR);
+}
+
+int pkg_store_ensure_package_root(FAR const char *name)
+{
+ char path[PATH_MAX];
+ int ret;
+
+ ret = pkg_store_format_package_root(path, sizeof(path), name);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ return pkg_store_mkdirs(path);
+}
+
+int pkg_store_ensure_version_dir(FAR const char *name,
+ FAR const char *version)
+{
+ char path[PATH_MAX];
+ int ret;
+
+ ret = pkg_store_ensure_package_root(name);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = pkg_store_format_version_path(path, sizeof(path), name, version);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ return pkg_store_mkdirs(path);
+}
+
+int pkg_store_format_index_path(FAR char *buffer, size_t size)
+{
+ return pkg_store_format(buffer, size, "%s", PKG_REPO_INDEX, "");
+}
+
+int pkg_store_format_installed_path(FAR char *buffer, size_t size)
+{
+ return pkg_store_format(buffer, size, "%s", PKG_REPO_INSTALLED, "");
+}
+
+int pkg_store_format_package_root(FAR char *buffer, size_t size,
+ FAR const char *name)
+{
+ return pkg_store_format(buffer, size, PKG_STORE_DIR "/%s", name, "");
+}
+
+int pkg_store_format_version_path(FAR char *buffer, size_t size,
+ FAR const char *name,
+ FAR const char *version)
+{
+ return pkg_store_format(buffer, size,
+ PKG_STORE_DIR "/%s/%s",
+ name, version);
+}
+
+int pkg_store_format_current_path(FAR char *buffer, size_t size,
+ FAR const char *name)
+{
+ return pkg_store_format(buffer, size,
+ PKG_STORE_DIR "/%s/current",
+ name, "");
+}
+
+int pkg_store_format_previous_path(FAR char *buffer, size_t size,
+ FAR const char *name)
+{
+ return pkg_store_format(buffer, size, PKG_STORE_DIR "/%s/previous", name,
+ "");
+}
+
+int pkg_store_format_txn_path(FAR char *buffer, size_t size,
+ FAR const char *name)
+{
+ return pkg_store_format(buffer, size, PKG_STORE_DIR "/%s/.txn", name, "");
+}
+
+int pkg_store_format_lock_path(FAR char *buffer, size_t size,
+ FAR const char *name)
+{
+ return pkg_store_format(buffer, size, PKG_STORE_DIR "/%s/.lock", name, "");
+}
+
+int pkg_store_format_download_path(FAR char *buffer, size_t size,
+ FAR const char *name,
+ FAR const char *version)
+{
+ return pkg_store_format(buffer, size, PKG_TMP_PKG_DIR "/%s-%s.npkg", name,
+ version);
+}
+
+int pkg_store_format_payload_path(FAR char *buffer, size_t size,
+ FAR const char *name,
+ FAR const char *version,
+ FAR const char *artifact)
+{
+ FAR const char *leaf;
+ int ret;
+
+ leaf = pkg_store_basename(artifact);
+ ret = snprintf(buffer, size, PKG_STORE_DIR "/%s/%s/%s",
+ name, version, leaf);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ if ((size_t)ret >= size)
+ {
+ return -ENAMETOOLONG;
+ }
+
+ return 0;
+}
+
+int pkg_store_format_manifest_path(FAR char *buffer, size_t size,
+ FAR const char *name,
+ FAR const char *version)
+{
+ return pkg_store_format(buffer, size, PKG_STORE_DIR "/%s/%s/manifest.json",
+ name, version);
+}
+
+int pkg_store_read_text(FAR const char *path, FAR char **buffer)
+{
+ FAR FILE *stream;
+ FAR char *data;
+ long length;
+ size_t nread;
+
+ if (buffer == NULL)
+ {
+ return -EINVAL;
+ }
+
+ *buffer = NULL;
+
+ stream = fopen(path, "rb");
+ if (stream == NULL)
+ {
+ return errno == ENOENT ? -ENOENT : -errno;
+ }
+
+ if (fseek(stream, 0, SEEK_END) < 0)
+ {
+ fclose(stream);
+ return -errno;
+ }
+
+ length = ftell(stream);
+ if (length < 0)
+ {
+ fclose(stream);
+ return -errno;
+ }
+
+ if (fseek(stream, 0, SEEK_SET) < 0)
+ {
+ fclose(stream);
+ return -errno;
+ }
+
+ data = malloc((size_t)length + 1);
+ if (data == NULL)
+ {
+ fclose(stream);
+ return -ENOMEM;
+ }
+
+ nread = fread(data, 1, (size_t)length, stream);
+ if (nread != (size_t)length)
+ {
+ int err = ferror(stream);
+
+ fclose(stream);
+ free(data);
+ return err ? -EIO : -EINVAL;
+ }
+
+ fclose(stream);
+
+ data[length] = '\0';
+ *buffer = data;
+ return 0;
+}
+
+int pkg_store_write_text_atomic(FAR const char *path, FAR const char *text)
+{
+ char tmp[PATH_MAX];
+ int fd;
+ int ret;
+
+ ret = snprintf(tmp, sizeof(tmp), "%s.tmp", path);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ if ((size_t)ret >= sizeof(tmp))
+ {
+ return -ENAMETOOLONG;
+ }
+
+ fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (fd < 0)
+ {
+ return -errno;
+ }
+
+ ret = pkg_store_write_all(fd, text, strlen(text));
+ if (ret < 0)
+ {
+ close(fd);
+ unlink(tmp);
+ return ret;
+ }
+
+ if (close(fd) < 0)
+ {
+ unlink(tmp);
+ return -errno;
+ }
+
+ if (rename(tmp, path) < 0)
+ {
+ unlink(tmp);
+ return -errno;
+ }
+
+ return 0;
+}
+
+int pkg_store_copy_file(FAR const char *src, FAR const char *dest)
+{
+ int infd;
+ int outfd;
+ int ret;
+ char buffer[512];
+
+ infd = open(src, O_RDONLY);
+ if (infd < 0)
+ {
+ return -errno;
+ }
+
+ outfd = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (outfd < 0)
+ {
+ ret = -errno;
+ close(infd);
+ return ret;
+ }
+
+ for (; ; )
+ {
+ ssize_t nread;
+
+ nread = read(infd, buffer, sizeof(buffer));
+ if (nread < 0)
+ {
+ if (errno == EINTR)
+ {
+ continue;
+ }
+
+ ret = -errno;
+ goto errout;
+ }
+
+ if (nread == 0)
+ {
+ break;
+ }
+
+ ret = pkg_store_write_all(outfd, buffer, (size_t)nread);
+ if (ret < 0)
+ {
+ goto errout;
+ }
+ }
+
+ close(infd);
+
+ if (close(outfd) < 0)
+ {
+ unlink(dest);
+ return -errno;
+ }
+
+ return 0;
+
+errout:
+ close(infd);
+ close(outfd);
+ unlink(dest);
+ return ret;
+}
+
+int pkg_store_remove_file(FAR const char *path)
+{
+ if (unlink(path) < 0)
+ {
+ return errno == ENOENT ? 0 : -errno;
+ }
+
+ return 0;
+}
diff --git a/system/nxpkg/pkg_txn.c b/system/nxpkg/pkg_txn.c
new file mode 100644
index 000000000..e20a8700f
--- /dev/null
+++ b/system/nxpkg/pkg_txn.c
@@ -0,0 +1,110 @@
+/****************************************************************************
+ * apps/system/nxpkg/pkg_txn.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership. The
+ * ASF licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <errno.h>
+#include <stdio.h>
+
+#include "pkg.h"
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+const char *pkg_txn_state_str(enum pkg_txn_state_e state)
+{
+ switch (state)
+ {
+ case PKG_TXN_IDLE:
+ return "IDLE";
+
+ case PKG_TXN_FETCHING:
+ return "FETCHING";
+
+ case PKG_TXN_VERIFIED:
+ return "VERIFIED";
+
+ case PKG_TXN_STAGED:
+ return "STAGED";
+
+ case PKG_TXN_COMPAT_OK:
+ return "COMPAT_OK";
+
+ case PKG_TXN_ACTIVATED:
+ return "ACTIVATED";
+
+ case PKG_TXN_CLEANUP:
+ return "CLEANUP";
+
+ case PKG_TXN_FAILED:
+ return "FAILED";
+
+ case PKG_TXN_RESTORE:
+ return "RESTORE";
+
+ default:
+ return "UNKNOWN";
+ }
+}
+
+int pkg_txn_write_state(FAR const char *name, enum pkg_txn_state_e state)
+{
+ char path[PATH_MAX];
+ char text[32];
+ int ret;
+
+ ret = pkg_store_format_txn_path(path, sizeof(path), name);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ ret = snprintf(text, sizeof(text), "%s\n", pkg_txn_state_str(state));
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ if ((size_t)ret >= sizeof(text))
+ {
+ return -ENAMETOOLONG;
+ }
+
+ return pkg_store_write_text_atomic(path, text);
+}
+
+int pkg_txn_clear_state(FAR const char *name)
+{
+ char path[PATH_MAX];
+ int ret;
+
+ ret = pkg_store_format_txn_path(path, sizeof(path), name);
+ if (ret < 0)
+ {
+ return ret;
+ }
+
+ return pkg_store_remove_file(path);
+}
diff --git a/tools/export_pkg_repo.py b/tools/export_pkg_repo.py
new file mode 100644
index 000000000..36d611434
--- /dev/null
+++ b/tools/export_pkg_repo.py
@@ -0,0 +1,229 @@
+#!/usr/bin/env python3
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership. The
+# ASF licenses this file to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Export built binaries into a package repository layout.
+
+The script copies built application or library artifacts into a
+server-friendly repository directory, computes SHA-256 digests, and emits an
+`index.json` compatible with the current `nxpkg` metadata parser.
+
+The repository can then be served by FTP, HTTP, or any static file transport.
+"""
+
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import shutil
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Dict, List
+
+VALID_TYPES = {"elf", "shared-lib"}
+PACKAGE_SPEC_HELP = (
+ "package must look like " "<name>:<version>:<elf|shared-lib>:<source>"
+)
+
+
+@dataclass
+class PackageSpec:
+ name: str
+ version: str
+ payload_type: str
+ source: Path
+
+
+def sha256_file(path: Path) -> str:
+ digest = hashlib.sha256()
+ with path.open("rb") as infile:
+ while True:
+ chunk = infile.read(65536)
+ if not chunk:
+ break
+
+ digest.update(chunk)
+
+ return digest.hexdigest()
+
+
+def parse_package_spec(value: str) -> PackageSpec:
+ parts = value.split(":", 3)
+ if len(parts) != 4:
+ raise argparse.ArgumentTypeError(PACKAGE_SPEC_HELP)
+
+ name, version, payload_type, source = parts
+ if payload_type not in VALID_TYPES:
+ raise argparse.ArgumentTypeError(
+ f"unsupported package type '{payload_type}', expected one of "
+ f"{', '.join(sorted(VALID_TYPES))}"
+ )
+
+ source_path = Path(source).expanduser().resolve()
+ if not source_path.is_file():
+ msg = f"source does not exist: {source_path}"
+ raise argparse.ArgumentTypeError(msg)
+
+ return PackageSpec(
+ name=name,
+ version=version,
+ payload_type=payload_type,
+ source=source_path,
+ )
+
+
+def artifact_relpath(arch: str, chip: str, compat: str,
+ spec: PackageSpec) -> Path: # fmt: skip
+ filename = spec.source.name
+ path = Path("artifacts") / arch / chip / compat
+ path = path / spec.name / spec.version / filename
+ return path
+
+
+def package_identity(package: Dict[str, str]) -> tuple:
+ return (
+ package["name"],
+ package["version"],
+ package["arch"],
+ package["compat"],
+ package["type"],
+ )
+
+
+def load_existing_packages(repo_dir: Path) -> List[dict]:
+ index_path = repo_dir / "index.json"
+
+ if not index_path.exists():
+ return []
+
+ root = json.loads(index_path.read_text(encoding="utf-8"))
+ if isinstance(root, list):
+ packages = root
+ else:
+ packages = root.get("packages")
+
+ if not isinstance(packages, list):
+ raise ValueError(f"invalid repository index format in {index_path}")
+
+ for item in packages:
+ if not isinstance(item, dict):
+ raise ValueError(f"invalid repository index entry in {index_path}")
+
+ return packages
+
+
+def emit_index(repo_dir: Path, packages: List[dict]) -> None:
+ index_path = repo_dir / "index.json"
+ payload = {"packages": packages}
+ index_path.write_text(
+ json.dumps(payload, indent=2, sort_keys=False) + "\n",
+ encoding="utf-8",
+ )
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "repo_dir",
+ type=Path,
+ help="Destination repository directory",
+ )
+ parser.add_argument(
+ "--arch",
+ required=True,
+ help="Target architecture string, for example xtensa",
+ )
+ parser.add_argument(
+ "--chip",
+ required=True,
+ help="Target chip/family string, for example esp32s3",
+ )
+ parser.add_argument(
+ "--compat",
+ required=True,
+ help="Target board/runtime identity, for example esp32s3-xiao",
+ )
+ parser.add_argument(
+ "--artifact-prefix",
+ default="",
+ help="Optional prefix prepended to artifact paths in index.json",
+ )
+ parser.add_argument(
+ "--package",
+ action="append",
+ required=True,
+ type=parse_package_spec,
+ help=PACKAGE_SPEC_HELP,
+ )
+ args = parser.parse_args()
+
+ repo_dir = args.repo_dir.expanduser().resolve()
+ repo_dir.mkdir(parents=True, exist_ok=True)
+
+ packages = load_existing_packages(repo_dir)
+ packages_by_id = {}
+ for package in packages:
+ packages_by_id[package_identity(package)] = package
+ prefix = args.artifact_prefix.rstrip("/")
+
+ for spec in args.package:
+ relpath = artifact_relpath(args.arch, args.chip, args.compat, spec)
+ destination = repo_dir / relpath
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copy2(spec.source, destination)
+
+ artifact = relpath.as_posix()
+ if prefix:
+ artifact = f"{prefix}/{artifact}"
+
+ package = {
+ "name": spec.name,
+ "version": spec.version,
+ "arch": args.arch,
+ "compat": args.compat,
+ "artifact": artifact,
+ "sha256": sha256_file(destination),
+ "type": spec.payload_type,
+ }
+ packages_by_id[package_identity(package)] = package
+
+ packages = sorted(
+ packages_by_id.values(),
+ key=lambda item: (
+ item["name"],
+ item["version"],
+ item["arch"],
+ item["compat"],
+ item["type"],
+ ),
+ )
+ emit_index(repo_dir, packages)
+
+ print(f"exported {len(packages)} package(s) to {repo_dir}")
+ for package in packages:
+ print(
+ f"- {package['name']} {package['version']} "
+ f"[{package['type']}] -> {package['artifact']}"
+ )
+
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())