Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/native-platform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,34 @@ jobs:
rpm=$(find "$bundle/rpm" -type f -name '*.rpm' -print -quit)
test -n "$deb"
test -n "$rpm"
deb_dependencies=$(dpkg-deb -f "$deb" Depends)
rpm_dependencies=$(rpm -qp --requires "$rpm")
printf 'DEB dependencies: %s\n' "$deb_dependencies"
printf 'RPM dependencies:\n%s\n' "$rpm_dependencies"
for dependency in \
libwebkit2gtk-4.1-0 libgtk-3-0 libc++1-18 libc++abi1-18 libunwind-18; do
case "$deb_dependencies" in
*"$dependency"*) ;;
*)
echo "DEB package is missing dependency: $dependency" >&2
exit 1
;;
esac
done
for dependency in libcxx llvm-libunwind; do
if ! grep -Fx "$dependency" <<< "$rpm_dependencies"; then
echo "RPM package is missing dependency: $dependency" >&2
exit 1
fi
done
# rpmbuild derives shared-library requirements from ELF metadata.
# DNF resolves these SONAME capabilities to the Fedora provider packages.
for capability in libwebkit2gtk-4.1.so.0 libgtk-3.so.0; do
if ! grep -F "${capability}()" <<< "$rpm_dependencies"; then
echo "RPM package is missing runtime capability: ${capability}()" >&2
exit 1
fi
done
dpkg-deb -x "$deb" "$scan/deb"
rpm -Kv "$rpm"
bsdtar -xf "$GITHUB_WORKSPACE/$rpm" -C "$scan/rpm"
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ an insufficient runtime from Microsoft; silent `/S` installations perform that
prerequisite step without a prompt. The installer does not bundle a fixed
WebView2 runtime and does not replace a newer installed version.

Linux packages use the system WebKitGTK runtime and LLVM 18 C++ runtime. The
RPM declares `webkit2gtk4.1`, `gtk3`, `libcxx`, and `llvm-libunwind`. The DEB
declares `libwebkit2gtk-4.1-0`, `libgtk-3-0`, `libc++1-18`, `libc++abi1-18`,
and `libunwind-18`. Debian or Ubuntu releases that do not provide the LLVM 18
runtime packages in their standard repositories require an appropriate LLVM
package source before the DEB can be installed. An older generic `libc++1`
package is not a compatible substitute.

## Command Line

The CLI exposes the same image and object operations for scripts and batch
Expand Down
13 changes: 13 additions & 0 deletions apps/application/include/axklib/application/image_sessions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,20 @@ struct ImageSessionRead {
std::shared_ptr<void> lease;
};

struct ImagePartitionCapacity {
std::uint32_t allocated_clusters{};
std::uint32_t free_clusters{};
std::uint32_t cluster_size_bytes{};
};

struct ImageContentItem {
std::string id;
std::optional<std::string> parent_id;
std::size_t depth{};
std::optional<std::uint8_t> partition_index;
std::optional<std::uint32_t> volume_directory_id;
std::optional<ImagePartitionCapacity> partition_capacity;
std::optional<std::uint64_t> size_bytes;
std::string kind;
std::string name;
std::string display_name;
Expand Down Expand Up @@ -166,6 +174,7 @@ struct ImageObjectItem {
std::string category_name;
std::string entry_name;
std::uint64_t stored_size_bytes{};
std::optional<std::uint64_t> size_with_dependencies_bytes;
std::optional<WaveformMetadata> waveform;
std::optional<SequenceMetadata> sequence;
};
Expand Down Expand Up @@ -243,6 +252,7 @@ struct ImageObjectDeletionImpact {
std::string volume_name;
std::string role;
std::string status;
bool requested{};
bool selected{};
std::uint64_t stored_size_bytes{};
std::uint64_t freed_clusters{};
Expand All @@ -267,6 +277,8 @@ struct ImageObjectDeletionInspection {
std::string image_id;
std::uint64_t revision{};
std::vector<std::string> target_object_ids;
std::vector<std::string> referrer_object_ids;
std::vector<std::string> cleanup_object_ids;
std::vector<std::string> selected_object_ids;
std::vector<ImageObjectDeletionImpact> impacts;
std::vector<ImageObjectDeletionReference> references;
Expand Down Expand Up @@ -427,6 +439,7 @@ class ImageSessionManager {
[[nodiscard]] Result<ImageObjectDeletionPlan> plan_deletion(std::string_view image_id, std::string_view owner_id,
std::uint64_t expected_revision,
const std::vector<std::string> &target_object_ids,
const std::vector<std::string> &referrer_object_ids,
const std::vector<std::string> &cleanup_object_ids);
[[nodiscard]] Result<ImageWaveDataOrphanInspection>
inspect_wave_data_orphans(std::string_view image_id, std::string_view owner_id, std::uint64_t expected_revision,
Expand Down
21 changes: 18 additions & 3 deletions apps/application/src/image_session_inspection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ axk::app::Result<axk::app::ImageSessionSummary> axk::app::ImageSessionManager::i

axk::app::Result<axk::app::ImageObjectDeletionPlan> axk::app::ImageSessionManager::plan_deletion(
std::string_view image_id, std::string_view owner_id, std::uint64_t expected_revision,
const std::vector<std::string> &target_object_ids, const std::vector<std::string> &cleanup_object_ids) {
const std::vector<std::string> &target_object_ids, const std::vector<std::string> &referrer_object_ids,
const std::vector<std::string> &cleanup_object_ids) {
const auto session = implementation_->owned(image_id, owner_id);
if (!session)
return std::unexpected(session.error());
Expand All @@ -82,6 +83,15 @@ axk::app::Result<axk::app::ImageObjectDeletionPlan> axk::app::ImageSessionManage
return std::unexpected(session_error("object_not_found", "deletion target does not exist"));
target_keys.push_back(found->second.key);
}
std::vector<std::string> referrer_keys;
referrer_keys.reserve(referrer_object_ids.size());
for (const auto &object_id : referrer_object_ids) {
const auto found = (*session)->snapshots_by_id.find(object_id);
if (found == (*session)->snapshots_by_id.end()) {
return std::unexpected(session_error("object_not_found", "deletion referrer object does not exist"));
}
referrer_keys.push_back(found->second.key);
}
std::vector<std::string> cleanup_keys;
cleanup_keys.reserve(cleanup_object_ids.size());
for (const auto &object_id : cleanup_object_ids) {
Expand All @@ -104,8 +114,10 @@ axk::app::Result<axk::app::ImageObjectDeletionPlan> axk::app::ImageSessionManage
if (container == nullptr)
return std::unexpected(
session_error("image_mutation_unsupported", "object deletion requires an SFS container"));
const auto inspected = inspect_object_deletion(
*container, catalog, graph, {.target_keys = std::move(target_keys), .cleanup_keys = std::move(cleanup_keys)});
const auto inspected = inspect_object_deletion(*container, catalog, graph,
{.target_keys = std::move(target_keys),
.referrer_keys = std::move(referrer_keys),
.cleanup_keys = std::move(cleanup_keys)});
if (!inspected)
return std::unexpected(session_error("deletion_invalid", inspected.error().message));

Expand Down Expand Up @@ -133,6 +145,8 @@ axk::app::Result<axk::app::ImageObjectDeletionPlan> axk::app::ImageSessionManage
result.image_id = std::string{image_id};
result.revision = expected_revision;
result.target_object_ids = target_object_ids;
result.referrer_object_ids = referrer_object_ids;
result.cleanup_object_ids = cleanup_object_ids;
result.selected_object_ids = map_keys(inspected->selected_keys);
result.estimated_freed_bytes = inspected->estimated_freed_bytes;
result.estimated_freed_clusters = inspected->estimated_freed_clusters;
Expand All @@ -149,6 +163,7 @@ axk::app::Result<axk::app::ImageObjectDeletionPlan> axk::app::ImageSessionManage
.volume_name = impact.volume_name,
.role = std::string{object_deletion_role_name(impact.role)},
.status = std::string{object_deletion_status_name(impact.status)},
.requested = impact.requested,
.selected = impact.selected,
.stored_size_bytes = impact.stored_size_bytes,
.freed_clusters = impact.freed_clusters,
Expand Down
99 changes: 98 additions & 1 deletion apps/application/src/image_session_open.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,64 @@

#include <charconv>
#include <iterator>
#include <limits>
#include <map>
#include <set>

#include "axklib/package_closure.hpp"

namespace {

std::optional<std::uint64_t>
exact_dependency_size(const axk::ObjectSnapshot &root, const axk::RelationshipGraph &graph,
const std::map<std::string, const axk::ObjectSnapshot *, std::less<>> &objects,
const std::map<std::string, std::uint64_t, std::less<>> &sizes) {
if (root.object.header.type != axk::ObjectType::prog && root.object.header.type != axk::ObjectType::sbac &&
root.object.header.type != axk::ObjectType::sbnk) {
return std::nullopt;
}
std::set<std::string, std::less<>> visited;
std::set<std::string, std::less<>> active;
std::uint64_t total{};
const auto visit = [&](const auto &self, const axk::ObjectSnapshot &object) -> bool {
const auto has_exact_closure_payload = [&]() {
switch (object.object.header.type) {
case axk::ObjectType::prog:
return std::holds_alternative<axk::CurrentProg>(object.object.payload);
case axk::ObjectType::sbac:
return std::holds_alternative<axk::CurrentSbac>(object.object.payload);
case axk::ObjectType::sbnk:
return std::holds_alternative<axk::CurrentSbnk>(object.object.payload);
default:
return true;
}
};
if (!has_exact_closure_payload())
return false;
if (active.contains(object.key))
return false;
if (!visited.emplace(object.key).second)
return true;
const auto size = sizes.find(object.key);
if (size == sizes.end() || size->second > std::numeric_limits<std::uint64_t>::max() - total)
return false;
total += size->second;
active.emplace(object.key);
auto required = axk::package_internal::required_relationships(object, graph, objects);
if (!required)
return false;
for (const auto *relationship : *required) {
const auto target = relationship->target_key ? objects.find(*relationship->target_key) : objects.end();
if (target == objects.end() || !self(self, *target->second))
return false;
}
active.erase(object.key);
return true;
};
return visit(visit, root) ? std::optional{total} : std::nullopt;
}

} // namespace

axk::app::Result<axk::app::ImageSessionSummary>
axk::app::ImageSessionManager::open(const ImageSourceRef &source, std::string owner_id,
Expand Down Expand Up @@ -163,10 +221,21 @@ axk::app::ImageSessionManager::open_with_companion_sources(const ImageSourceRef
auto graph = axk::build_relationship_graph(inventory->catalog);
auto tree = axk::build_content_tree(*media, inventory->catalog, graph);
std::unordered_map<std::uint8_t, std::string> partition_names;
std::unordered_map<std::uint8_t, ImagePartitionCapacity> partition_capacities;
if (const auto *sfs = std::get_if<axk::Container>(&media->storage())) {
partition_names.reserve(sfs->partitions().size());
for (const auto &partition : sfs->partitions())
partition_capacities.reserve(sfs->partitions().size());
for (const auto &partition : sfs->partitions()) {
partition_names.emplace(partition.index.value, partition.name);
if (partition.allocation.free_space) {
const auto &free_space = *partition.allocation.free_space;
partition_capacities.emplace(
partition.index.value,
ImagePartitionCapacity{.allocated_clusters = free_space.allocated_cluster_count,
.free_clusters = free_space.free_cluster_count,
.cluster_size_bytes = free_space.cluster_size_bytes});
}
}
}

auto session = std::make_shared<Implementation::Session>();
Expand All @@ -193,6 +262,12 @@ axk::app::ImageSessionManager::open_with_companion_sources(const ImageSourceRef
}
for (const auto &descriptor : inventory->objects)
session->descriptors_by_id.emplace(object_ids.at(descriptor.key), descriptor);
std::map<std::string, const axk::ObjectSnapshot *, std::less<>> objects_by_key;
std::map<std::string, std::uint64_t, std::less<>> sizes_by_key;
for (const auto &object : inventory->catalog.objects)
objects_by_key.emplace(object.key, &object);
for (const auto &descriptor : inventory->objects)
sizes_by_key.emplace(descriptor.key, descriptor.size);
session->catalog_issues = inventory->catalog.issues;
if (const auto *sfs = std::get_if<axk::Container>(&media->storage())) {
const auto orphan_report = axk::analyze_waveform_orphans(*sfs, inventory->catalog, graph);
Expand All @@ -216,6 +291,7 @@ axk::app::ImageSessionManager::open_with_companion_sources(const ImageSourceRef
item.name = object.object.header.name;
item.format = object_format_name(object.object.format);
item.stored_size_bytes = session->descriptors_by_id.at(item.id).size;
item.size_with_dependencies_bytes = exact_dependency_size(object, graph, objects_by_key, sizes_by_key);
if (object.placement) {
item.partition_index = object.partition.value;
item.partition_name = object.placement->partition_name;
Expand Down Expand Up @@ -315,11 +391,20 @@ axk::app::ImageSessionManager::open_with_companion_sources(const ImageSourceRef
const auto converted = std::from_chars(first, last, result);
return converted.ec == std::errc{} && converted.ptr == last ? std::optional{result} : std::nullopt;
}();
const auto partition_capacity = [&]() -> std::optional<ImagePartitionCapacity> {
if (node.node_type != "partition" || !partition_index)
return std::nullopt;
if (const auto found = partition_capacities.find(*partition_index); found != partition_capacities.end())
return found->second;
return std::nullopt;
}();
ImageContentItem item{.id = id,
.parent_id = parent_id,
.depth = depth,
.partition_index = partition_index,
.volume_directory_id = volume_directory_id,
.partition_capacity = partition_capacity,
.size_bytes = std::nullopt,
.kind = node.node_type,
.name = canonical_name,
.display_name = node.display_name,
Expand Down Expand Up @@ -355,6 +440,18 @@ axk::app::ImageSessionManager::open_with_companion_sources(const ImageSourceRef
std::ranges::sort(scoped_indices);
const auto unique_end = std::ranges::unique(scoped_indices).begin();
scoped_indices.erase(unique_end, scoped_indices.end());
if (session->content[item_index].kind == "volume") {
std::uint64_t size{};
const auto complete = std::ranges::all_of(scoped_indices, [&](std::size_t object_index) {
const auto object_size = session->objects[object_index].stored_size_bytes;
if (object_size > std::numeric_limits<std::uint64_t>::max() - size)
return false;
size += object_size;
return true;
});
if (complete)
session->content[item_index].size_bytes = size;
}
if (session->content[item_index].kind == "volume" && !session->content[item_index].partition_index &&
!scoped_indices.empty()) {
const auto inferred_partition_index = session->objects[scoped_indices.front()].partition_index;
Expand Down
Loading
Loading