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())

Reply via email to