Something went wrong. Try again.
Completely reimplimented in zig (eventually)
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100const std = @import("std");const mem = std.mem;const fmt = std.fmt;const ascii = std.ascii;const Io = std.Io;const Allocator = mem.Allocator;const File = Io.File;const ArrayList = std.ArrayList;const Dir = Io.Dir;const Decompress = std.compress.flate.Decompress;const Uri = std.Uri;const Client = std.http.Client;const HeaderIterator = std.http.HeaderIterator;const Reader = Io.Reader;const Writer = Io.Writer;const Order = std.math.Order;
pub const PackType = enum { dfp, tool, unknown };
pub const Release = struct { version: []const u8, installed: bool, ptype: PackType,};
pub const Pack = struct { name: []const u8, vendor: []const u8, releases: ArrayList(Release), devices: ArrayList([]const u8),};
pub const IndexEntry = struct { name: []const u8, vendor: []const u8, version: []const u8, ptype: PackType, devices: []const []const u8, ide_prereqs: []const []const u8,};
pub const Update = struct { name: []const u8, vendor: []const u8, version: []const u8,};
pub const ListOptions = struct { all: bool = false, updates: bool = false, dfps: bool = false, tps: bool = false, device_name: ?[]const u8 = null,};
const REMOTE_REPO = "https://packs.download.microchip.com/";
fn parseIntTok(tok: []const u8) i64 { return fmt.parseInt(i64, tok, 10) catch 0;}
fn preRelease(tokens: []const []const u8) []const u8 { if (tokens.len != 3) return ""; const patch = tokens[2]; const dash = mem.findScalar(u8, patch, '-') orelse return ""; const rest = patch[dash + 1 ..]; const plus = mem.findScalar(u8, rest, '+') orelse return rest; return rest[0..plus];}
fn splitStr(allocator: Allocator, s: []const u8, sep: u8) ![]const []const u8 { var out: ArrayList([]const u8) = .empty; var it = mem.splitScalar(u8, s, sep); while (it.next()) |part| try out.append(allocator, part); return out.toOwnedSlice(allocator);}
pub fn versionCompare(a: []const u8, b: []const u8, allocator: Allocator) !i32 { const a_tokens = try splitStr(allocator, a, '.'); const b_tokens = try splitStr(allocator, b, '.');
const common = @min(@min(a_tokens.len, b_tokens.len), 3); var i: usize = 0; while (i < common) : (i += 1) { const c = parseIntTok(a_tokens[i]) - parseIntTok(b_tokens[i]); if (c < 0) return -1; if (c > 0) return 1; }
if (a_tokens.len != b_tokens.len) { if (common < a_tokens.len) return 1; if (common < b_tokens.len) return -1; return 0; }
const a_pre = preRelease(a_tokens); const b_pre = preRelease(b_tokens); return switch (mem.order(u8, a_pre, b_pre)) { .lt => -1, .eq => 0, .gt => 1, };}
fn attrValue(buf: []const u8, start: usize, attr: []const u8) ?[]const u8 { const tag_end = mem.findScalarPos(u8, buf, start, '>') orelse return null; const tag = buf[start..tag_end]; var needle_buf: [128]u8 = undefined; if (attr.len + 2 > needle_buf.len) return null; @memcpy(needle_buf[0..attr.len], attr); needle_buf[attr.len] = '='; needle_buf[attr.len + 1] = '"'; const needle = needle_buf[0 .. attr.len + 2]; const idx = mem.find(u8, tag, needle) orelse return null; const val_start = idx + needle.len; const val_end = mem.findScalarPos(u8, tag, val_start, '"') orelse return null; return tag[val_start..val_end];}
fn elemText(allocator: Allocator, buf: []const u8, tag: []const u8) !?[]const u8 { const open = try fmt.allocPrint(allocator, "<{s}>", .{tag}); const close = try fmt.allocPrint(allocator, "</{s}>", .{tag}); const s = mem.find(u8, buf, open) orelse return null; const start = s + open.len; const e = mem.findPos(u8, buf, start, close) orelse return null; return buf[start..e];}
fn hasStr(buf: []const u8, kw: []const u8) bool { return mem.find(u8, buf, kw) != null;}
fn parsePdsc(allocator: Allocator, content: []const u8) !?Pack { const name = try elemText(allocator, content, "name") orelse return null; const vendor = try elemText(allocator, content, "vendor") orelse return null;
var ptype: PackType = .unknown; if (hasStr(content, "Device Family Package Microchip")) ptype = .dfp else if (hasStr(content, "Tool Pack Microchip")) ptype = .tool;
var pack: Pack = .{ .name = try allocator.dupe(u8, name), .vendor = try allocator.dupe(u8, vendor), .releases = .empty, .devices = .empty, };
var pos: usize = 0; while (mem.findPos(u8, content, pos, "<release")) |s| { if (attrValue(content, s, "version")) |v| { try pack.releases.append(allocator, .{ .version = try allocator.dupe(u8, v), .installed = true, .ptype = ptype, }); } pos = s + 8; }
pos = 0; while (mem.findPos(u8, content, pos, "<device ")) |s| { if (attrValue(content, s, "Dname")) |dn| try pack.devices.append(allocator, try allocator.dupe(u8, dn)); pos = s + 8; }
return pack;}
fn parseIndex(allocator: Allocator, content: []const u8, entries: *ArrayList(IndexEntry)) !void { var pos: usize = 0; while (mem.findPos(u8, content, pos, "<pdsc")) |s| { const name = attrValue(content, s, "atmel:name") orelse attrValue(content, s, "name"); const version = attrValue(content, s, "version");
const next = mem.findPos(u8, content, s + 1, "<pdsc") orelse mem.findPos(u8, content, s + 1, "</idx>") orelse content.len;
var ptype: PackType = .unknown; if (hasStr(content[s..next], "Device Family Package Microchip")) ptype = .dfp else if (hasStr(content[s..next], "Tool Pack Microchip")) ptype = .tool;
var devices: ArrayList([]const u8) = .empty; var dpos = s; while (mem.findPos(u8, content, dpos, "<atmel:device ")) |ds| { if (ds >= next) break; if (attrValue(content, ds, "name")) |dn| try devices.append(allocator, try allocator.dupe(u8, dn)); dpos = ds + 13; }
var ide_prereqs: ArrayList([]const u8) = .empty; var ppos = s; while (mem.findPos(u8, content, ppos, "<atmel:prerequisite ")) |ps| { if (ps >= next) break; const comp = attrValue(content, ps, "component") orelse ""; if (mem.eql(u8, comp, "MPLAB X IDE")) if (attrValue(content, ps, "version")) |pv| try ide_prereqs.append(allocator, try allocator.dupe(u8, pv)); ppos = ps + 19; }
if (name) |n| { const vendor = try vendorFromNameAttr( allocator, attrValue(content, s, "name"), ); try entries.append(allocator, .{ .name = try allocator.dupe(u8, n), .vendor = vendor, .version = version orelse "", .ptype = ptype, .devices = try devices.toOwnedSlice(allocator), .ide_prereqs = try ide_prereqs.toOwnedSlice(allocator), }); } pos = s + 5; }}
fn vendorFromNameAttr(allocator: Allocator, name_attr: ?[]const u8) ![]const u8 { const na = name_attr orelse return "Microchip"; if (mem.findScalar(u8, na, '.')) |dot| return allocator.dupe(u8, na[0..dot]); return "Microchip";}
// Binary index cache//// Format (little-endian):// [4] magic "IDX1"// [8] u64 checksum (wyhash of original XML)// [4] u32 entry count// per entry:// [2] u16 name len, [N] name bytes// [2] u16 vendor len, [N] vendor bytes// [2] u16 version len, [N] version bytes// [1] u8 ptype (0=dfp, 1=tool, 2=unknown)// [2] u16 device count// per device: [2] u16 len, [N] bytes// [2] u16 prereq count// per prereq: [2] u16 len, [N] bytes
const CACHE_MAGIC = "IDX1";
fn writeIndexCache(io: Io, allocator: Allocator, repo_path: []const u8, checksum: u64, entries: []const IndexEntry) !void { var buf: ArrayList(u8) = .empty;
try buf.appendSlice(allocator, CACHE_MAGIC); try buf.appendNTimes(allocator, 0, 8); std.mem.writePackedInt(u64, buf.items[4..12], 0, checksum, .little); const count: u32 = @intCast(entries.len); try buf.appendNTimes(allocator, 0, 4); std.mem.writePackedInt(u32, buf.items[12..16], 0, count, .little);
for (entries) |e| { try writeString(allocator, &buf, e.name); try writeString(allocator, &buf, e.vendor); try writeString(allocator, &buf, e.version); try buf.append(allocator, @intFromEnum(e.ptype));
try buf.appendNTimes(allocator, 0, 2); const dcount: u16 = @intCast(e.devices.len); std.mem.writePackedInt(u16, buf.items[buf.items.len - 2 ..], 0, dcount, .little); for (e.devices) |d| try writeString(allocator, &buf, d);
try buf.appendNTimes(allocator, 0, 2); const pcount: u16 = @intCast(e.ide_prereqs.len); std.mem.writePackedInt(u16, buf.items[buf.items.len - 2 ..], 0, pcount, .little); for (e.ide_prereqs) |p| try writeString(allocator, &buf, p); }
const cache_path = try fmt.allocPrint(allocator, "{s}/index.cache", .{repo_path}); try Dir.cwd().writeFile(io, .{ .sub_path = cache_path, .data = buf.items });}
fn writeString(allocator: Allocator, buf: *ArrayList(u8), s: []const u8) !void { try buf.appendNTimes(allocator, 0, 2); const len: u16 = @intCast(@min(s.len, std.math.maxInt(u16))); std.mem.writePackedInt(u16, buf.items[buf.items.len - 2 ..], 0, len, .little); try buf.appendSlice(allocator, s[0..len]);}
const IndexCache = struct { checksum: u64, entries: ArrayList(IndexEntry),};
fn readIndexCache(io: Io, allocator: Allocator, repo_path: []const u8) !IndexCache { var entries: ArrayList(IndexEntry) = .empty; const cache_path = try fmt.allocPrint(allocator, "{s}/index.cache", .{repo_path}); const cwd: Dir = .cwd(); const data = try cwd.readFileAlloc(io, cache_path, allocator, .unlimited); defer allocator.free(data);
if (data.len < 16) return error.InvalidCache; if (!mem.eql(u8, data[0..4], CACHE_MAGIC)) return error.InvalidCache;
const checksum = mem.readInt(u64, data[4..12], .little); const count = mem.readInt(u32, data[12..16], .little); var pos: usize = 16;
for (0..count) |_| { if (pos + 2 > data.len) break; var r = readString(data, pos); const name = r.str; pos = r.end; r = readString(data, pos); const vendor = r.str; pos = r.end; r = readString(data, pos); const version = r.str; pos = r.end; if (pos + 1 > data.len) break; const ptype: PackType = switch (data[pos]) { 0 => .dfp, 1 => .tool, else => .unknown, }; pos += 1;
if (pos + 2 > data.len) break; const dcount = mem.readInt(u16, data[pos..][0..2], .little); pos += 2; var devices: ArrayList([]const u8) = .empty; for (0..dcount) |_| { r = readString(data, pos); try devices.append(allocator, try allocator.dupe(u8, r.str)); pos = r.end; }
if (pos + 2 > data.len) break; const pcount = mem.readInt(u16, data[pos..][0..2], .little); pos += 2; var ide_prereqs: ArrayList([]const u8) = .empty; for (0..pcount) |_| { r = readString(data, pos); try ide_prereqs.append(allocator, try allocator.dupe(u8, r.str)); pos = r.end; }
try entries.append(allocator, .{ .name = try allocator.dupe(u8, name), .vendor = try allocator.dupe(u8, vendor), .version = try allocator.dupe(u8, version), .ptype = ptype, .devices = try devices.toOwnedSlice(allocator), .ide_prereqs = try ide_prereqs.toOwnedSlice(allocator), }); }
return .{ .checksum = checksum, .entries = entries };}
fn readString(data: []const u8, pos: usize) struct { str: []const u8, end: usize } { if (pos + 2 > data.len) return .{ .str = "", .end = data.len }; const len = mem.readInt(u16, data[pos..][0..2], .little); const start = pos + 2; const end = @min(start + len, data.len); return .{ .str = data[start..end], .end = end };}
fn indexChecksum(content: []const u8) u64 { return std.hash.Wyhash.hash(0, content);}
fn mergePack(allocator: Allocator, packs: *ArrayList(Pack), p: Pack) !void { for (packs.items) |*existing| { if (mem.eql(u8, existing.name, p.name) and mem.eql(u8, existing.vendor, p.vendor)) return existing.releases.appendSlice(allocator, p.releases.items); } try packs.append(allocator, p);}
fn walkRepo(io: Io, allocator: Allocator, repo_path: []const u8, packs: *ArrayList(Pack)) !void { var dir: Dir = try .openDirAbsolute(io, repo_path, .{ .iterate = true }); defer dir.close(io); var walker = try dir.walk(allocator); defer walker.deinit();
while (try walker.next(io)) |entry| { if (entry.kind != .file) continue; if (!mem.endsWith(u8, entry.basename, ".pdsc")) continue; const content = try dir.readFileAlloc(io, entry.path, allocator, .unlimited); if (try parsePdsc(allocator, content)) |p| try mergePack(allocator, packs, p); }}
fn loadIndexCache(io: Io, allocator: Allocator, repo_path: []const u8) ArrayList(IndexEntry) { const cache = readIndexCache(io, allocator, repo_path) catch return .empty; return cache.entries;}
fn loadIndexChecksum(io: Io, allocator: Allocator, repo_path: []const u8) ?u64 { const cache = readIndexCache(io, allocator, repo_path) catch return null; return cache.checksum;}
pub fn scanInstalledPacks(io: Io, allocator: Allocator, user_repo: []const u8, system_repo: []const u8) !ArrayList(Pack) { var packs: ArrayList(Pack) = .empty; if (user_repo.len > 0) try walkRepo(io, allocator, user_repo, &packs); if (system_repo.len > 0) try walkRepo(io, allocator, system_repo, &packs); return packs;}
pub fn loadIndexEntries(io: Io, allocator: Allocator, user_repo: []const u8, _: []const u8) !ArrayList(IndexEntry) { var entries: ArrayList(IndexEntry) = .empty; if (user_repo.len > 0) { entries = loadIndexCache(io, allocator, user_repo); } return entries;}
// ---------------------------------------------------------------------------// Query helpers// ---------------------------------------------------------------------------
pub fn isInstalledVersion(packs: []const Pack, name: []const u8, vendor: []const u8, version: []const u8) bool { for (packs) |p| if (mem.eql(u8, p.name, name) and mem.eql(u8, p.vendor, vendor)) for (p.releases.items) |r| if (r.installed and std.mem.eql(u8, r.version, version)) return true; return false;}
pub fn indexLatestVersion(entries: []const IndexEntry, name: []const u8) ?[]const u8 { for (entries) |e| if (ascii.eqlIgnoreCase(e.name, name)) return if (e.version.len > 0) e.version else null; return null;}
pub fn maxInstalledVersion(allocator: Allocator, p: *const Pack) !?[]const u8 { var best: ?[]const u8 = null; for (p.releases.items) |r| { if (!r.installed) continue; if (best == null or try versionCompare(r.version, best.?, allocator) > 0) best = r.version; } return best;}
pub fn latestReleaseVersion(allocator: Allocator, p: *const Pack) ![]const u8 { if (p.releases.items.len == 0) return "None"; var best = p.releases.items[0].version; for (p.releases.items[1..]) |r| { if (try versionCompare(r.version, best, allocator) > 0) best = r.version; } return best;}
pub fn latestReleaseType(allocator: Allocator, p: *const Pack) !PackType { if (p.releases.items.len == 0) return .unknown; var best = &p.releases.items[0]; for (p.releases.items[1..]) |*r| { if (try versionCompare(r.version, best.version, allocator) > 0) best = r; } return best.ptype;}
pub fn typeString(ptype: PackType) []const u8 { return switch (ptype) { .tool => "Tool Pack", else => "Device Family Pack", };}
pub fn findIndexEntry(entries: []const IndexEntry, name: []const u8) ?IndexEntry { for (entries) |e| if (mem.eql(u8, e.name, name)) return e; return null;}
pub fn fileExists(io: Io, path: []const u8) bool { const cwd = Dir.cwd(); var f = cwd.openFile(io, path, .{}) catch return false; f.close(io); return true;}
// ---------------------------------------------------------------------------// Uninstall// ---------------------------------------------------------------------------
pub fn deletePackDir( io: Io, allocator: Allocator, user_repo: []const u8, vendor: []const u8, name: []const u8, version: []const u8,) !void { const path = try fmt.allocPrint(allocator, "{s}/{s}/{s}/{s}", .{ user_repo, vendor, name, version }); Dir.deleteTree(.cwd(), io, path) catch {};}
// ---------------------------------------------------------------------------// Remote index management// ---------------------------------------------------------------------------
fn sanitizeEtag(etag: []const u8, allocator: Allocator) ![]const u8 { var out: ArrayList(u8) = .empty; for (etag) |c| if (c != '"') try out.append(allocator, c); return out.toOwnedSlice(allocator);}
fn readMetapmEtag(io: Io, allocator: Allocator, user_repo: []const u8) !?[]const u8 { const path = try fmt.allocPrint(allocator, "{s}/metapm.xml", .{user_repo}); const cwd: Dir = .cwd(); const content = try cwd.readFileAlloc(io, path, allocator, .unlimited); const needle = "name=\"ETag\" value=\""; const idx = mem.find(u8, content, needle) orelse return null; const start = idx + needle.len; const end = mem.findScalarPos(u8, content, start, '"') orelse return null; return @as([]const u8, try allocator.dupe(u8, content[start..end]));}
fn writeMetapmXml(io: Io, allocator: Allocator, user_repo: []const u8, etag: []const u8) !void { const path = try fmt.allocPrint(allocator, "{s}/metapm.xml", .{user_repo}); const content = try fmt.allocPrint(allocator, "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" ++ "<metapm description=\"Packslib metadata file\">\n" ++ " <property name=\"ETag\" value=\"{s}\"/>\n" ++ "</metapm>\n", .{etag}); try Dir.cwd().writeFile(io, .{ .sub_path = path, .data = content });}
fn gunzip(allocator: Allocator, bytes: []const u8) ![]u8 { var src: Reader = .fixed(bytes); var window: [std.compress.flate.max_window_len]u8 = undefined; var dc: Decompress = .init(&src, .gzip, &window); var out: ArrayList(u8) = .empty; try dc.reader.appendRemainingUnlimited(allocator, &out); return out.items;}
pub const UpdateIndexResult = enum { up_to_date, updated, error_no_etag,};
pub fn updateIndex(io: Io, allocator: Allocator, user_repo: []const u8) UpdateIndexResult { const stored_etag = readMetapmEtag(io, allocator, user_repo) catch null;
var client: Client = .{ .allocator = allocator, .io = io }; defer client.deinit();
const url = "https://packs.download.microchip.com/index.idx.gz"; const uri: Uri = std.Uri.parse(url) catch return .error_no_etag;
var req = client.request(.GET, uri, .{}) catch return .error_no_etag; defer req.deinit(); req.sendBodiless() catch return .error_no_etag;
var redirect_buffer: [8192]u8 = undefined; var response = req.receiveHead(&redirect_buffer) catch return .error_no_etag;
var etag: ?[]const u8 = null; var it: HeaderIterator = .init(response.head.bytes); while (it.next()) |h| { if (std.ascii.eqlIgnoreCase(h.name, "etag")) { etag = sanitizeEtag(h.value, allocator) catch null; break; } }
const conn_etag = etag orelse return .error_no_etag;
if (stored_etag) |se| { if (std.ascii.eqlIgnoreCase(se, conn_etag)) { const cache_valid = readIndexCache(io, allocator, user_repo) catch null; if (cache_valid != null) { _ = response.reader(&.{}); return .up_to_date; } } }
var transfer_buffer: [4096]u8 = undefined; const body_reader = response.reader(&transfer_buffer); var gz: ArrayList(u8) = .empty; body_reader.appendRemainingUnlimited(allocator, &gz) catch return .error_no_etag;
const index = gunzip(allocator, gz.items) catch return .error_no_etag; const checksum = indexChecksum(index); const stored_checksum = loadIndexChecksum(io, allocator, user_repo);
if (stored_checksum == checksum) return .up_to_date;
var entries: ArrayList(IndexEntry) = .empty; parseIndex(allocator, index, &entries) catch return .error_no_etag; writeIndexCache(io, allocator, user_repo, checksum, entries.items) catch return .error_no_etag;
writeMetapmXml(io, allocator, user_repo, conn_etag) catch return .error_no_etag;
return .updated;}
// ---------------------------------------------------------------------------// Download and install// ---------------------------------------------------------------------------
fn downloadPack(io: Io, allocator: Allocator, url: []const u8) ![]u8 { var client: Client = .{ .allocator = allocator, .io = io }; defer client.deinit();
var w: Writer.Allocating = .init(allocator); const result = client.fetch(.{ .location = .{ .url = url }, .response_writer = &w.writer, }) catch return error.DownloadFailed;
if (result.status != .ok) return error.DownloadFailed;
const list = w.toArrayList(); return list.items;}
fn extractAtpack(io: Io, _: Allocator, zip_bytes: []const u8, dest_dir: []const u8) !void { const tmp = "/tmp/packslibdl.atpack"; const cwd: Dir = .cwd(); try cwd.writeFile(io, .{ .sub_path = tmp, .data = zip_bytes });
var file = try cwd.openFile(io, tmp, .{}); defer file.close(io);
var buf: [64 * 1024]u8 = undefined; var reader: File.Reader = .init(file, io, &buf);
var dest = try Dir.openDirAbsolute(io, dest_dir, .{ .iterate = true }); defer dest.close(io);
try std.zip.extract(dest, &reader, .{});
try cwd.deleteTree(io, tmp);}
pub fn installPackFromRemote(io: Io, allocator: Allocator, name: []const u8, vendor: []const u8, version: []const u8, user_repo: []const u8) !void { const url = try fmt.allocPrint(allocator, "{s}{s}.{s}.{s}.atpack", .{ REMOTE_REPO, vendor, name, version }); const bytes = try downloadPack(io, allocator, url); const dest = try fmt.allocPrint(allocator, "{s}/{s}/{s}/{s}", .{ user_repo, vendor, name, version }); try Dir.cwd().createDirPath(io, dest); try extractAtpack(io, allocator, bytes, dest);}
pub fn installFromDisk(io: Io, allocator: Allocator, path: []const u8, install_location: []const u8) !void { const cwd = Dir.cwd(); const bytes = try cwd.readFileAlloc(io, path, allocator, .unlimited);
const tmpdir = try fmt.allocPrint(allocator, "/tmp/packslibdisk_{d}", .{@intFromPtr(&bytes[0])}); try cwd.createDirPath(io, tmpdir); try extractAtpack(io, allocator, bytes, tmpdir);
var name: ?[]const u8 = null; var vendor: ?[]const u8 = null; var version: ?[]const u8 = null; { var dir: Dir = try .openDirAbsolute(io, tmpdir, .{ .iterate = true }); defer dir.close(io); var walker = try dir.walk(allocator); defer walker.deinit(); while (try walker.next(io)) |entry| { if (entry.kind != .file or !mem.endsWith(u8, entry.basename, ".pdsc")) continue; const content = try dir.readFileAlloc(io, entry.path, allocator, .unlimited); if (try parsePdsc(allocator, content)) |p| { name = p.name; vendor = p.vendor; var best: ?[]const u8 = null; for (p.releases.items) |r| { if (best == null or try versionCompare(r.version, best.?, allocator) > 0) best = r.version; } version = best; break; } } }
const n = name orelse return error.InvalidPackFile; const v = vendor orelse return error.InvalidPackFile; const ver = version orelse return error.InvalidPackFile;
const dest = try fmt.allocPrint(allocator, "{s}/{s}/{s}/{s}", .{ install_location, v, n, ver }); try cwd.createDirPath(io, dest); try extractAtpack(io, allocator, bytes, dest);
try cwd.deleteTree(io, tmpdir);}
// ---------------------------------------------------------------------------// Update detection// ---------------------------------------------------------------------------
pub fn packUpdates(io: Io, allocator: Allocator, user_repo: []const u8, system_repo: []const u8) !ArrayList(Update) { const packs = try scanInstalledPacks(io, allocator, user_repo, system_repo); const entries = try loadIndexEntries(io, allocator, user_repo, system_repo);
for (entries.items) |ie| { if (ie.version.len == 0) continue; for (packs.items) |*p| { if (std.mem.eql(u8, p.name, ie.name)) { try p.releases.append(allocator, .{ .version = ie.version, .installed = false, .ptype = ie.ptype }); break; } } }
var updates: ArrayList(Update) = .empty; for (packs.items) |p| { const latest_installed = (try maxInstalledVersion(allocator, &p)) orelse continue; const latest = try latestReleaseVersion(allocator, &p); if (try versionCompare(latest_installed, latest, allocator) < 0) { try updates.append(allocator, .{ .name = p.name, .vendor = p.vendor, .version = latest, }); } } return updates;}
// ---------------------------------------------------------------------------// Device-to-pack resolution// ---------------------------------------------------------------------------
pub fn findPackForDevice(allocator: Allocator, entries: []const IndexEntry, device: []const u8) ![]Update { var result: ArrayList(Update) = .empty;
for (entries) |e| { if (e.version.len == 0 or e.ptype != .dfp) continue; for (e.devices) |d| { if (ascii.eqlIgnoreCase(d, device)) { try appendUnique(allocator, &result, .{ .name = e.name, .vendor = e.vendor, .version = e.version }); break; } } }
if (result.items.len > 0) return result.toOwnedSlice(allocator);
const lower_device = try ascii.allocLowerString(allocator, device); for (entries) |e| { if (e.version.len == 0 or e.ptype != .dfp) continue; for (e.devices) |d| { const lower_d = try ascii.allocLowerString(allocator, d); if (mem.find(u8, lower_d, lower_device) != null) { try appendUnique(allocator, &result, .{ .name = e.name, .vendor = e.vendor, .version = e.version }); break; } } }
return result.toOwnedSlice(allocator);}
fn appendUnique(allocator: Allocator, list: *ArrayList(Update), item: Update) !void { for (list.items) |existing| { if (mem.eql(u8, existing.name, item.name)) return; } try list.append(allocator, item);}
fn appendRightAligned(out: *ArrayList(u8), allocator: Allocator, s: []const u8, width: usize) !void { if (s.len < width) { var i: usize = 0; while (i < width - s.len) : (i += 1) try out.append(allocator, ' '); } try out.appendSlice(allocator, s);}
fn appendLeftAligned(out: *ArrayList(u8), allocator: Allocator, s: []const u8, width: usize) !void { try out.appendSlice(allocator, s); if (s.len < width) { var i: usize = 0; while (i < width - s.len) : (i += 1) try out.append(allocator, ' '); }}
fn compareNameIgnoreCase(a: []const u8, b: []const u8) Order { const n = @min(a.len, b.len); var i: usize = 0; while (i < n) : (i += 1) { const ca = ascii.toLower(a[i]); const cb = ascii.toLower(b[i]); if (ca < cb) return .lt; if (ca > cb) return .gt; } if (a.len < b.len) return .lt; if (a.len > b.len) return .gt; return .eq;}
fn joinComma(allocator: Allocator, strings: []const []const u8) ![]const u8 { var out: ArrayList(u8) = .empty; for (strings, 0..) |s, i| { if (i != 0) try out.appendSlice(allocator, ", "); try out.appendSlice(allocator, s); } return out.toOwnedSlice(allocator);}
// ---------------------------------------------------------------------------// Version range matching (for IDE prerequisites)// ---------------------------------------------------------------------------
fn versionRangeMatches(allocator: Allocator, version: []const u8, range: []const u8) !bool { if (mem.findScalar(u8, range, '-') == null) { var v = range; if (v.len >= 2 and (v[0] == '[' or v[0] == '(') and (v[v.len - 1] == ']' or v[v.len - 1] == ')')) { v = v[1 .. v.len - 1]; } return (try versionCompare(version, v, allocator)) == 0; } const dash = mem.findScalar(u8, range, '-').?; const min_part = range[0..dash]; const max_part = range[dash + 1 ..];
if (min_part.len > 0) { const inclusive = min_part[0] == '['; const min_ver = min_part[1..]; if (inclusive) { if (try versionCompare(version, min_ver, allocator) < 0) return false; } else { if (try versionCompare(version, min_ver, allocator) <= 0) return false; } } if (max_part.len > 0) { const inclusive = max_part[max_part.len - 1] == ']'; const max_ver = max_part[0 .. max_part.len - 1]; if (inclusive) { if (try versionCompare(version, max_ver, allocator) > 0) return false; } else { if (try versionCompare(version, max_ver, allocator) >= 0) return false; } } return true;}
fn meetsIdePrereqs(allocator: Allocator, component_version: []const u8, prereqs: []const []const u8) !bool { if (prereqs.len == 0) return false; for (prereqs) |r| { if (try versionRangeMatches(allocator, component_version, r)) return true; } return false;}
// ---------------------------------------------------------------------------// Dependent tools resolution// ---------------------------------------------------------------------------
pub fn dependentTools(allocator: Allocator, component_version: []const u8, pack: *const Pack, entries: []const IndexEntry) ArrayList([]const u8) { var tools: ArrayList([]const u8) = .empty; for (entries) |e| { if (e.ptype != .tool or e.version.len == 0) continue; if (!(meetsIdePrereqs(allocator, component_version, e.ide_prereqs) catch false)) continue; var match = false; outer: { for (e.devices) |d| { for (pack.devices.items) |pd| { if (ascii.eqlIgnoreCase(d, pd)) { match = true; break :outer; } } } } if (match) { tools.append(allocator, std.fmt.allocPrint(allocator, "{s} ({s})", .{ e.name, e.version }) catch continue) catch {}; } } return tools;}
pub fn listPacks( io: Io, allocator: Allocator, user_repo: []const u8, system_repo: []const u8, lopts: ListOptions, out: *ArrayList(u8),) !void { var packs: ArrayList(Pack) = .empty; if (user_repo.len > 0) try walkRepo(io, allocator, user_repo, &packs); if (system_repo.len > 0) try walkRepo(io, allocator, system_repo, &packs);
var index_entries: ArrayList(IndexEntry) = .empty; if (user_repo.len > 0) { index_entries = loadIndexCache(io, allocator, user_repo); } if (system_repo.len > 0 and index_entries.items.len == 0) { index_entries = loadIndexCache(io, allocator, system_repo); }
for (index_entries.items) |ie| { if (ie.version.len == 0) continue; var found = false; for (packs.items) |*p| { if (mem.eql(u8, p.name, ie.name)) { try p.releases.append(allocator, .{ .version = ie.version, .installed = false, .ptype = ie.ptype }); found = true; break; } } if (!found) { var np: Pack = .{ .name = ie.name, .vendor = "", .releases = .empty, .devices = .empty, }; try np.releases.append(allocator, .{ .version = ie.version, .installed = false, .ptype = ie.ptype }); try packs.append(allocator, np); } }
const display_only_installed = !lopts.all; const display_only_updates = lopts.updates; const dfps = lopts.dfps; const tps = lopts.tps; const show_all_types = !(dfps != tps);
var matching: ArrayList(*Pack) = .empty; for (packs.items) |*p| { const latest_installed = try maxInstalledVersion(allocator, p); const installed = latest_installed != null;
if (installed) { const latest = try latestReleaseVersion(allocator, p); if (display_only_updates and try versionCompare(latest_installed.?, latest, allocator) >= 0) continue; } else if (display_only_installed) continue;
if (show_all_types) { try matching.append(allocator, p); } else { const lt = try latestReleaseType(allocator, p); if (dfps and lt == .dfp) { try matching.append(allocator, p); } else if (tps and lt == .tool) { try matching.append(allocator, p); } } }
if (matching.items.len == 0) { try out.appendSlice(allocator, "No packs match current filtering criteria!\n"); return; }
{ const status: []const u8 = if (display_only_installed) (if (display_only_updates) "available pack updates for" else "installed") else "all"; const type_str: []const u8 = if (show_all_types) "packs" else if (dfps) "Device Family Packs" else "Tool Packs"; try out.appendSlice(allocator, "List of "); try out.appendSlice(allocator, status); try out.append(allocator, ' '); try out.appendSlice(allocator, type_str); try out.append(allocator, ' '); try out.append(allocator, ':'); try out.append(allocator, '\n'); }
mem.sort(*Pack, matching.items, {}, struct { fn lessThan(_: void, a: *Pack, b: *Pack) bool { return compareNameIgnoreCase(a.name, b.name) == .lt; } }.lessThan);
var max_width: usize = 0; for (matching.items) |p| { if (p.name.len > max_width) max_width = p.name.len; }
for (matching.items) |p| { const latest_installed = try maxInstalledVersion(allocator, p) orelse "None"; const latest = try latestReleaseVersion(allocator, p); const ptype = try latestReleaseType(allocator, p);
try appendLeftAligned(out, allocator, p.name, max_width); try out.appendSlice(allocator, " Installed "); try appendRightAligned(out, allocator, latest_installed, 9); try out.appendSlice(allocator, " Latest "); try appendRightAligned(out, allocator, latest, 9); try out.append(allocator, ' '); try out.appendSlice(allocator, typeString(ptype)); try out.append(allocator, '\n'); }}
// ---------------------------------------------------------------------------// Describe pack// ---------------------------------------------------------------------------
pub fn describePack( io: Io, allocator: Allocator, name: []const u8, component_version: []const u8, user_repo: []const u8, system_repo: []const u8, out: *ArrayList(u8),) ?[]const u8 { const packs = scanInstalledPacks( io, allocator, user_repo, system_repo, ) catch |err| return fmt.allocPrint(allocator, "Error reading installed packs: {}", .{err}) catch "Error reading packs"; const entries = loadIndexEntries( io, allocator, user_repo, system_repo, ) catch |err| return fmt.allocPrint(allocator, "Error reading index: {}", .{err}) catch "Error reading index";
var found: ?*Pack = null; resolve: { for (packs.items) |*p| { if (ascii.eqlIgnoreCase(p.name, name)) { found = p; break :resolve; } } for (packs.items) |*p| { for (p.devices.items) |d| { if (ascii.eqlIgnoreCase(d, name)) { found = p; break :resolve; } } } } const p = found orelse return fmt.allocPrint( allocator, "Unknown pack or unsupported device: {s}", .{name}, ) catch "Unknown pack";
const ptype = latestReleaseType(allocator, p) catch .unknown; const is_dfp = ptype != .tool;
const installed = maxInstalledVersion(allocator, p) catch null orelse "None"; const latest = latestReleaseVersion(allocator, p) catch "None";
out.appendSlice(allocator, "Pack Summary Information:\n") catch {}; appendLeftAligned(out, allocator, p.name, p.name.len) catch {}; out.appendSlice(allocator, " Installed ") catch {}; appendRightAligned(out, allocator, installed, 9) catch {}; out.appendSlice(allocator, " Latest ") catch {}; appendRightAligned(out, allocator, latest, 9) catch {}; out.append(allocator, ' ') catch {}; out.appendSlice(allocator, typeString(ptype)) catch {}; out.append(allocator, '\n') catch {};
if (!is_dfp) return null; if (p.devices.items.len > 0) { const devs = joinComma(allocator, p.devices.items) catch ""; out.appendSlice(allocator, fmt.allocPrint( allocator, "\nDevice Summary for {s} version {s}:\n", .{ p.name, installed }, ) catch "") catch {}; out.appendSlice(allocator, devs) catch {}; out.append(allocator, '\n') catch {}; }
const tools = dependentTools( allocator, component_version, p, entries.items, ); if (tools.items.len > 0) { const tools_str = joinComma(allocator, tools.items) catch ""; out.appendSlice(allocator, fmt.allocPrint( allocator, "\nDependent Tools Summary for {s} version {s}:\n", .{ p.name, installed }, ) catch "") catch {}; out.appendSlice(allocator, tools_str) catch {}; out.append(allocator, '\n') catch {}; }
return null;}