mirror of
https://github.com/apache/nuttx-apps.git
synced 2026-08-01 20:29:00 +00:00
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 <gargaviral99@gmail.com>
770 lines
18 KiB
C
770 lines
18 KiB
C
/****************************************************************************
|
|
* 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;
|
|
}
|