const std = @import("std"); const mem = std.mem; const ascii = std.ascii; const process = std.process; const meta = std.meta; const Init = process.Init; const Io = std.Io; const File = Io.File; const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const Environ = process.Environ; const cli = @import("cli"); const engine = @import("engine.zig"); const style = @import("style.zig"); /// Print a styled diagnostic on stderr and exit(1). fn fail(init: Init, comptime fmt: []const u8, args: anytype) noreturn { var buffer: [256]u8 = undefined; var w = File.stderr().writer(init.io, &buffer); style.errPrefix(init.io, &w.interface) catch {}; w.interface.print(fmt, args) catch {}; w.interface.flush() catch {}; process.exit(1); } const Writers = struct { stdout: File = .stdout(), stderr: File = .stderr(), const E = enum { stdout, stderr }; pub fn write(self: *Writers, io: Io, writer: E, bytes: []const u8) !void { const file = switch (writer) { .stdout => self.stdout, .stderr => self.stderr, }; var buffer: [4096]u8 = undefined; var w = file.writer(io, &buffer); try w.interface.writeAll(bytes); try w.interface.flush(); } pub fn print(self: *Writers, io: Io, writer: E, comptime fmt: []const u8, args: anytype) !void { const file = switch (writer) { .stdout => self.stdout, .stderr => self.stderr, }; var buffer: [256]u8 = undefined; var w = file.writer(io, &buffer); try w.interface.print(fmt, args); try w.interface.flush(); } }; const MPLABX_VERSION = "6.35"; const Filter = enum { installed, all, updated }; const Restrict = enum { tools, devices }; fn resolveUserRepo(allocator: Allocator, env: *Environ.Map) []const u8 { if (env.get("packslib.userpacksfolder")) |p| return p; if (env.get("MPLABX_PACKS_DIR")) |p| return p; const home = env.get("HOME") orelse return ""; return std.fmt.allocPrint(allocator, "{s}/.mchp_packs", .{home}) catch ""; } fn resolveSystemRepo(env: *Environ.Map) []const u8 { return env.get("packslib.packsfolder") orelse ""; } fn isPath(name: []const u8) bool { return mem.find(u8, name, "/") != null or mem.endsWith(u8, name, ".atpack"); } pub fn list( init: Init, filter: cli.Param( "filter", Filter, .optional(.installed), "Filter by status: installed, all, updates", ), restrict_val: cli.Param( "restrict", ?Restrict, .optional(null), "Filter by type: tools, devices", ), device_name: cli.Param( "device-name", ?[]const u8, .optional(null), "Filter by device name", ), ) !void { const allocator = init.arena.allocator(); const user_repo = resolveUserRepo(allocator, init.environ_map); const system_repo = resolveSystemRepo(init.environ_map); var out: ArrayList(u8) = .empty; const lopts: engine.ListOptions = .{ .all = filter.value == .all, .updates = filter.value == .updated, .dfps = restrict_val.value == .devices and device_name.value == null, .tps = restrict_val.value == .tools and device_name.value == null, .device_name = device_name.value, }; var writers: Writers = .{}; engine.listPacks(init.io, allocator, user_repo, system_repo, lopts, &out) catch |err| { fail(init, "listing packs: {}\n", .{err}); }; try writers.write(init.io, .stdout, out.items); } pub fn install( init: Init, name: cli.Param( "name", []const u8, .required, "Pack name, device name, or path to .atpack file", ), version: cli.Param( "version", []const u8, .optional("latest"), "Package version (remote only)", ), vendor: cli.Param( "vendor", []const u8, .optional("Microchip"), "Package vendor (remote only)", ), location: cli.Param( "location", []const u8, .optional(""), "Install location for disk installs (defaults to user repo)", ), ) !void { const allocator = init.arena.allocator(); const user_repo = resolveUserRepo(allocator, init.environ_map); const system_repo = resolveSystemRepo(init.environ_map); var writers: Writers = .{}; // 1. Path detection if (isPath(name.value)) { if (!engine.fileExists(init.io, name.value)) { fail(init, "file not found: {s}\n", .{name.value}); } const install_location = if (location.value.len > 0) location.value else user_repo; engine.installFromDisk(init.io, allocator, name.value, install_location) catch |err| { fail(init, "failed to install pack from disk: {s} ({})\n", .{ name.value, err }); }; return; } // Update remote index (unless installing from disk, which we're not here) if (user_repo.len > 0) { _ = engine.updateIndex(init.io, allocator, user_repo); } // 2. Pack name lookup const entries = engine.loadIndexEntries(init.io, allocator, user_repo, system_repo) catch |err| { fail(init, "reading pack index: {}\n", .{err}); }; if (engine.indexLatestVersion(entries.items, name.value)) |_| { // Pack found in index — resolve version const resolve_version = if (mem.eql(u8, version.value, "latest")) engine.indexLatestVersion(entries.items, name.value).? else version.value; const packs = engine.scanInstalledPacks( init.io, allocator, user_repo, system_repo, ) catch |err| { fail(init, "reading installed packs: {}\n", .{err}); }; if (engine.isInstalledVersion(packs.items, name.value, vendor.value, resolve_version)) { fail(init, "pack {s} version {s} is already installed\n", .{ name.value, resolve_version }); } engine.installPackFromRemote( init.io, allocator, name.value, vendor.value, resolve_version, user_repo, ) catch |err| { fail(init, "unable to install pack {s} {s}: {}\n", .{ name.value, resolve_version, err }); }; const out = std.fmt.allocPrint( allocator, "Installed {s} {s}\n", .{ name.value, resolve_version }, ) catch "Installed\n"; try writers.write(init.io, .stdout, out); return; } // 3. Device name lookup const device_matches = engine.findPackForDevice( allocator, entries.items, name.value, ) catch |err| { fail(init, "searching for device: {}\n", .{err}); }; if (device_matches.len == 1) { const upd = device_matches[0]; const packs = engine.scanInstalledPacks(init.io, allocator, user_repo, system_repo) catch |err| { fail(init, "reading installed packs: {}\n", .{err}); }; if (engine.isInstalledVersion(packs.items, upd.name, upd.vendor, upd.version)) { fail(init, "pack {s} version {s} is already installed\n", .{ upd.name, upd.version }); } engine.installPackFromRemote( init.io, allocator, upd.name, upd.vendor, upd.version, user_repo, ) catch |err| { fail(init, "unable to install pack {s} {s}: {}\n", .{ upd.name, upd.version, err }); }; const out = std.fmt.allocPrint( allocator, "Installed {s} {s}\n", .{ upd.name, upd.version }, ) catch "Installed\n"; try writers.write(init.io, .stdout, out); return; } if (device_matches.len > 1) { var ebuf: [256]u8 = undefined; var ew = File.stderr().writer(init.io, &ebuf); try style.errPrefix(init.io, &ew.interface); try ew.interface.print("multiple packs found for device '{s}':\n", .{name.value}); for (device_matches) |upd| { try ew.interface.print(" {s} {s} ({s})\n", .{ upd.name, upd.version, upd.vendor }); } try ew.interface.writeAll("specify the pack name directly\n"); try ew.interface.flush(); process.exit(1); } fail(init, "no pack found for device: {s}\n", .{name.value}); } pub fn uninstall( init: Init, name: cli.Param( "name", []const u8, .required, "Package name", ), version: cli.Param( "version", []const u8, .required, "Package version", ), vendor: cli.Param( "vendor", []const u8, .optional("Microchip"), "Package vendor", ), ) !void { const allocator = init.arena.allocator(); const user_repo = resolveUserRepo(allocator, init.environ_map); const system_repo = resolveSystemRepo(init.environ_map); var writers: Writers = .{}; const packs = engine.scanInstalledPacks( init.io, allocator, user_repo, system_repo, ) catch |err| { fail(init, "reading installed packs: {}\n", .{err}); }; if (!engine.isInstalledVersion(packs.items, name.value, vendor.value, version.value)) { fail(init, "selected pack version is not installed: {s} version {s}\n", .{ name.value, version.value }); } try engine.deletePackDir( init.io, allocator, user_repo, vendor.value, name.value, version.value, ); const out = std.fmt.allocPrint(allocator, "Uninstalled {s} {s}\n", .{ name.value, version.value }) catch "Uninstalled\n"; try writers.write(init.io, .stdout, out); } pub fn details( init: Init, name: cli.Param( "name", []const u8, .required, "Pack or device name", ), ) !void { const allocator = init.arena.allocator(); const user_repo = resolveUserRepo(allocator, init.environ_map); const system_repo = resolveSystemRepo(init.environ_map); const packs_list = engine.scanInstalledPacks(init.io, allocator, user_repo, system_repo) catch |err| { fail(init, "reading installed packs: {}\n", .{err}); }; const entries = engine.loadIndexEntries(init.io, allocator, user_repo, system_repo) catch |err| { fail(init, "reading pack index: {}\n", .{err}); }; var found: ?*engine.Pack = null; resolve: { for (packs_list.items) |*p| { if (mem.eql(u8, p.name, name.value)) { found = p; break :resolve; } } // Fall back to matching a supported device name. for (packs_list.items) |*p| { for (p.devices.items) |d| { if (ascii.eqlIgnoreCase(d, name.value)) { found = p; break :resolve; } } } } const p = found orelse fail(init, "unknown pack or unsupported device: {s}\n", .{name.value}); const ui = style.Ui.detect(init.io, init.environ_map); var out_buf: [8192]u8 = undefined; var w = File.stdout().writer(init.io, &out_buf); const o = &w.interface; const ptype = engine.latestReleaseType(allocator, p) catch .unknown; const is_dfp = (ptype == .dfp or ptype == .unknown); const installed = engine.maxInstalledVersion(allocator, p) catch null orelse "None"; const latest = engine.latestReleaseVersion(allocator, p) catch "None"; // Header block. try ui.strong(o, p.name); try o.writeAll("\n"); try printField(o, ui, "Vendor", p.vendor, .plain); try printField(o, ui, "Type", engine.typeString(ptype), .plain); try printField(o, ui, "Installed", installed, if (mem.eql(u8, installed, latest)) .good else .warn); try printField(o, ui, "Latest", latest, .plain); // Device list, flowed into columns. if (is_dfp and p.devices.items.len > 0) { try o.writeAll("\n"); try ui.header(o, "Devices"); try o.print(" ({d})\n", .{p.devices.items.len}); const rows = try style.flowColumns(allocator, p.devices.items, ui.width -| 3); defer style.freeRows(allocator, rows); for (rows) |row| { try o.writeAll(" "); try o.writeAll(row); try o.writeAll("\n"); } } // Tools that support this pack's devices. if (is_dfp) { const tools = engine.dependentTools(allocator, MPLABX_VERSION, p, entries.items); if (tools.items.len > 0) { try o.writeAll("\n"); try ui.header(o, "Dependent tools"); try o.print(" ({d})\n", .{tools.items.len}); const rows = try style.flowColumns(allocator, tools.items, ui.width -| 3); defer style.freeRows(allocator, rows); for (rows) |row| { try o.writeAll(" "); try o.writeAll(row); try o.writeAll("\n"); } } } try w.interface.flush(); } const FieldTone = enum { plain, good, warn }; fn printField(o: *std.Io.Writer, ui: style.Ui, comptime label: []const u8, value: []const u8, tone: FieldTone) !void { try ui.dim(o, " "); try o.print(label ++ ":", .{}); // Align values in a fixed column regardless of label width. var pad: usize = 10 -| label.len; while (pad > 0) : (pad -= 1) try o.writeAll(" "); switch (tone) { .plain => try o.writeAll(value), .good => try ui.good(o, value), .warn => try ui.warn(o, value), } try o.writeAll("\n"); } pub fn update(init: Init) !void { const allocator = init.arena.allocator(); const user_repo = resolveUserRepo(allocator, init.environ_map); const system_repo = resolveSystemRepo(init.environ_map); var writers: Writers = .{}; const index_result = engine.updateIndex(init.io, allocator, user_repo); switch (index_result) { .up_to_date => try writers.write(init.io, .stdout, "No changes.\n"), .updated => try writers.write(init.io, .stdout, "Index updated.\n"), .error_no_etag => fail(init, "could not update index\n", .{}), } const updates = engine.packUpdates(init.io, allocator, user_repo, system_repo) catch |err| { fail(init, "checking for updates: {}\n", .{err}); }; for (updates.items) |u| { engine.installPackFromRemote(init.io, allocator, u.name, u.vendor, u.version, user_repo) catch |err| { fail(init, "unable to install pack {s} {s}: {}\n", .{ u.name, u.version, err }); }; const out = std.fmt.allocPrint(allocator, "Installed {s} {s}\n", .{ u.name, u.version }) catch "Installed\n"; try writers.write(init.io, .stdout, out); } if (updates.items.len == 0) { try writers.write(init.io, .stdout, "All packs are up to date.\n"); } }