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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ mere search python
# install more packages
mere install -p test busybox curl git

# see what's in your profile
mere profile list
# see which package versions are active
mere profile packages
mere profile packages -p test

# inspect the store
ls /mere/store/
Expand Down
112 changes: 112 additions & 0 deletions src/cli/commands/profile.zig
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ const list_meta = command.CommandMeta{
.description = "List all profiles",
};

/// Packages subcommand metadata
const packages_meta = command.CommandMeta{
.name = "packages",
.description = "List packages in a profile's active state",
.flags = &[_]types.Flag{
.{
.name = "profile",
.short = 'p',
.description = "Profile to inspect (default: system)",
.flag_type = .string,
.value_name = "name",
},
},
};

/// Create subcommand metadata
const create_meta = command.CommandMeta{
.name = "create",
Expand Down Expand Up @@ -221,6 +236,99 @@ pub fn handleList(ctx: *mere.Context, args: *const types.ParsedArgs) MereError!t
};
}

/// List package names and versions from a profile's active realized state.
pub fn handlePackages(ctx: *mere.Context, args: *const types.ParsedArgs) MereError!types.CommandResult {
const profile_name = args.getString("profile") orelse "system";
const profile_dir = std.fs.path.join(ctx.allocator, &.{ ctx.root_path, "mere", "profiles", profile_name }) catch {
return MereError.OutOfMemory;
};
defer ctx.allocator.free(profile_dir);

const store_root = std.fs.path.join(ctx.allocator, &.{ ctx.root_path, "mere", "store" }) catch {
return MereError.OutOfMemory;
};
defer ctx.allocator.free(store_root);

var active_generation: ?u32 = null;
const active_path = if (std.mem.eql(u8, profile_name, "system")) blk: {
active_generation = generation_mod.getCurrentGeneration(profile_dir) catch |err| {
ctx.setDiagnosticContextFmt(profile_dir, "failed to read current generation: {s}", .{@errorName(err)});
return try command.errorResult(ctx, err, "failed to read current generation");
};
const generation = active_generation orelse {
return types.CommandResult{
.success = true,
.message = try std.fmt.allocPrint(ctx.allocator, "Profile '{s}' has no active generation", .{profile_name}),
};
};
break :blk generation_mod.getGenerationPath(ctx.allocator, profile_dir, generation) catch return MereError.OutOfMemory;
} else blk: {
const root_path = profile_mod.getRootPath(ctx.allocator, profile_dir) catch return MereError.OutOfMemory;
std.Io.Dir.accessAbsolute(path.currentIo(), root_path, .{}) catch |err| switch (err) {
error.FileNotFound => {
ctx.allocator.free(root_path);
return types.CommandResult{
.success = true,
.message = try std.fmt.allocPrint(ctx.allocator, "Profile '{s}' has no active realized state", .{profile_name}),
};
},
else => {
ctx.withDiagnosticContext(mere.errors.DiagnosticContext.init().withSubject(root_path));
const failure = try command.errorResult(ctx, err, "failed to inspect profile state");
ctx.allocator.free(root_path);
return failure;
},
};
break :blk root_path;
};
defer ctx.allocator.free(active_path);

var manifest = generation_mod.readManifest(ctx.allocator, store_root, active_path) catch |err| {
ctx.withDiagnosticContext(mere.errors.DiagnosticContext.init().withSubject(active_path));
return try command.errorResult(ctx, err, "failed to read active profile manifest");
};
defer manifest.deinit();

const packages = ctx.allocator.dupe(generation_mod.PackageEntry, manifest.packages.items) catch {
return MereError.OutOfMemory;
};
defer ctx.allocator.free(packages);
std.mem.sort(generation_mod.PackageEntry, packages, {}, struct {
fn lessThan(_: void, left: generation_mod.PackageEntry, right: generation_mod.PackageEntry) bool {
const name_order = std.mem.order(u8, left.name, right.name);
if (name_order != .eq) return name_order == .lt;
const version_order = std.mem.order(u8, left.version, right.version);
if (version_order != .eq) return version_order == .lt;
return left.release < right.release;
}
}.lessThan);

var output: std.ArrayList(u8) = .empty;
defer output.deinit(ctx.allocator);
var out_buf: std.Io.Writer.Allocating = .fromArrayList(ctx.allocator, &output);
const out = &out_buf.writer;

if (active_generation) |generation| {
out.print("Packages in profile '{s}' (gen-{d}):\n", .{ profile_name, generation }) catch return MereError.OutOfMemory;
} else {
out.print("Packages in profile '{s}':\n", .{profile_name}) catch return MereError.OutOfMemory;
}

if (packages.len == 0) {
out.writeAll(" (none)\n") catch return MereError.OutOfMemory;
} else {
for (packages) |pkg| {
out.print(" {s} {s}-{d}\n", .{ pkg.name, pkg.version, pkg.release }) catch return MereError.OutOfMemory;
}
}
output = out_buf.toArrayList();

return types.CommandResult{
.success = true,
.message = try ctx.allocator.dupe(u8, output.items),
};
}

/// Create profile handler
pub fn handleCreate(ctx: *mere.Context, args: *const types.ParsedArgs) MereError!types.CommandResult {
if (args.positional.len < 1) {
Expand Down Expand Up @@ -513,6 +621,9 @@ pub fn createCommand(allocator: std.mem.Allocator) !*command.Command {
const list_cmd = try allocator.create(command.Command);
list_cmd.* = command.Command.init(allocator, list_meta, handleList);

const packages_cmd = try allocator.create(command.Command);
packages_cmd.* = command.Command.init(allocator, packages_meta, handlePackages);

const create_cmd = try allocator.create(command.Command);
create_cmd.* = command.Command.init(allocator, create_meta, handleCreate);

Expand All @@ -523,6 +634,7 @@ pub fn createCommand(allocator: std.mem.Allocator) !*command.Command {
apply_cmd.* = command.Command.init(allocator, apply_meta, handleApply);

try profile_cmd.addSubcommand(list_cmd);
try profile_cmd.addSubcommand(packages_cmd);
try profile_cmd.addSubcommand(create_cmd);
try profile_cmd.addSubcommand(delete_cmd);
try profile_cmd.addSubcommand(apply_cmd);
Expand Down
113 changes: 113 additions & 0 deletions src/cli/commands_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,116 @@ test "pin add fails without ever touching gc-roots when the store lock can't be
defer testing.allocator.free(gc_roots_dir);
try testing.expectError(error.FileNotFound, std.Io.Dir.accessAbsolute(mere.path.currentIo(), gc_roots_dir, .{}));
}

fn writeProfileManifest(
ctx: *mere.Context,
profile_path: []const u8,
generation: ?u32,
packages: []const struct { name: []const u8, version: []const u8, release: u32 },
) !void {
var profile_dir = try mere.path.makePathAndOpenDir(profile_path);
profile_dir.close(mere.path.currentIo());

var manifest = if (generation) |number|
mere.generation.GenerationManifest.init(ctx.allocator, number)
else
mere.generation.GenerationManifest.initRoot(ctx.allocator);
defer manifest.deinit();

for (packages) |pkg| {
const store_path = try std.fmt.allocPrint(ctx.allocator, "/mere/store/{s}-{s}-{s}", .{ "a" ** 64, pkg.name, pkg.version });
defer ctx.allocator.free(store_path);
try manifest.addPackage(pkg.name, pkg.version, pkg.release, "x86_64", store_path, "a" ** 64);
}
try mere.generation.writeManifest(ctx.allocator, profile_path, &manifest);
}

test "profile packages lists the active system generation deterministically" {
const testing = std.testing;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();

var path_buf: [std.fs.max_path_bytes]u8 = undefined;
const root_len = try tmp.dir.realPath(mere.path.currentIo(), &path_buf);
var ctx = mere.Context.init(testing.allocator, path_buf[0..root_len]);
defer ctx.deinit();

const profile_dir = try std.fs.path.join(testing.allocator, &.{ ctx.root_path, "mere", "profiles", "system" });
defer testing.allocator.free(profile_dir);
const gen_path = try std.fs.path.join(testing.allocator, &.{ profile_dir, "gen-2" });
defer testing.allocator.free(gen_path);
try writeProfileManifest(&ctx, gen_path, 2, &.{
.{ .name = "zlib", .version = "1.3.1", .release = 2 },
.{ .name = "busybox", .version = "1.36.1", .release = 4 },
});
var profile_handle = try std.Io.Dir.openDirAbsolute(mere.path.currentIo(), profile_dir, .{});
defer profile_handle.close(mere.path.currentIo());
try profile_handle.symLink(mere.path.currentIo(), "gen-2", mere.generation.CURRENT_SYMLINK, .{});

var args = types.ParsedArgs.init(testing.allocator);
defer args.deinit();
const result = try profile_cmd.handlePackages(&ctx, &args);
defer if (result.message) |message| testing.allocator.free(message);

try testing.expect(result.success);
try testing.expectEqualStrings(
"Packages in profile 'system' (gen-2):\n busybox 1.36.1-4\n zlib 1.3.1-2\n",
result.message.?,
);
}

test "profile packages reads a named profile's active root" {
const testing = std.testing;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();

var path_buf: [std.fs.max_path_bytes]u8 = undefined;
const root_len = try tmp.dir.realPath(mere.path.currentIo(), &path_buf);
var ctx = mere.Context.init(testing.allocator, path_buf[0..root_len]);
defer ctx.deinit();

const root_path = try std.fs.path.join(testing.allocator, &.{ ctx.root_path, "mere", "profiles", "tools", "root" });
defer testing.allocator.free(root_path);
try writeProfileManifest(&ctx, root_path, null, &.{
.{ .name = "git", .version = "2.51.0", .release = 1 },
});

var args = types.ParsedArgs.init(testing.allocator);
defer args.deinit();
try args.flags.put("profile", .{ .string = "tools" });
const result = try profile_cmd.handlePackages(&ctx, &args);
defer if (result.message) |message| testing.allocator.free(message);

try testing.expect(result.success);
try testing.expectEqualStrings("Packages in profile 'tools':\n git 2.51.0-1\n", result.message.?);
}

test "profile packages distinguishes missing and empty active state" {
const testing = std.testing;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();

var path_buf: [std.fs.max_path_bytes]u8 = undefined;
const root_len = try tmp.dir.realPath(mere.path.currentIo(), &path_buf);
var ctx = mere.Context.init(testing.allocator, path_buf[0..root_len]);
defer ctx.deinit();
var args = types.ParsedArgs.init(testing.allocator);
defer args.deinit();

const missing = try profile_cmd.handlePackages(&ctx, &args);
defer if (missing.message) |message| testing.allocator.free(message);
try testing.expectEqualStrings("Profile 'system' has no active generation", missing.message.?);

const profile_dir = try std.fs.path.join(testing.allocator, &.{ ctx.root_path, "mere", "profiles", "system" });
defer testing.allocator.free(profile_dir);
const gen_path = try std.fs.path.join(testing.allocator, &.{ profile_dir, "gen-1" });
defer testing.allocator.free(gen_path);
try writeProfileManifest(&ctx, gen_path, 1, &.{});
var profile_handle = try std.Io.Dir.openDirAbsolute(mere.path.currentIo(), profile_dir, .{});
defer profile_handle.close(mere.path.currentIo());
try profile_handle.symLink(mere.path.currentIo(), "gen-1", mere.generation.CURRENT_SYMLINK, .{});

const empty = try profile_cmd.handlePackages(&ctx, &args);
defer if (empty.message) |message| testing.allocator.free(message);
try testing.expectEqualStrings("Packages in profile 'system' (gen-1):\n (none)\n", empty.message.?);
}