Skip to content

Commit dccfcf6

Browse files
feat: support zls master compilation (#156)
* feat: support zls master compilation * fix url
1 parent 6c3911c commit dccfcf6

4 files changed

Lines changed: 428 additions & 0 deletions

File tree

src/core/install.zig

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,14 @@ fn resolve_zls_release(
379379
}
380380
const platform_str = platform_str_buffer[0..platform_str_temp.len];
381381

382+
// Stable releases live in the GitHub releases index. Master and pinned
383+
// dev builds live behind the `select-version` endpoint, which returns a
384+
// different JSON shape (per-platform tarball + shasum + size).
385+
if (util_tool.is_master_like_version(version)) {
386+
try resolve_zls_master_release(ctx, release, version, platform_str);
387+
return;
388+
}
389+
382390
const version_data = try fetch_zls_version_data(ctx, platform_str, version);
383391

384392
var version_path_buffer = try ctx.scratch(.path);
@@ -396,6 +404,35 @@ fn resolve_zls_release(
396404
assert(release.signature_url() == null);
397405
}
398406

407+
fn resolve_zls_master_release(
408+
ctx: *context.CliContext,
409+
release: *Release,
410+
version: []const u8,
411+
platform_str: []const u8,
412+
) !void {
413+
assert(version.len > 0);
414+
assert(version.len < 100);
415+
assert(platform_str.len > 0);
416+
assert(util_tool.is_master_like_version(version));
417+
418+
const version_data = try fetch_zls_master_version_data(ctx, platform_str, version);
419+
420+
var version_path_buffer = try ctx.scratch(.path);
421+
defer version_path_buffer.release();
422+
const version_root = try util_data.get_zvm_zls_version(version_path_buffer);
423+
424+
release.init(.zls);
425+
try release.set_version(version_data.version());
426+
try release.set_extract_path_from_parts(ctx, version_root, release.version());
427+
release.hash = version_data.shasum;
428+
release.size = version_data.size;
429+
try release.add_download_url(version_data.tarball());
430+
431+
assert(release.download_urls_count == 1);
432+
assert(release.signature_url() == null);
433+
assert(release.hash != null);
434+
}
435+
399436
fn install_release(
400437
ctx: *context.CliContext,
401438
release: *const Release,
@@ -924,3 +961,126 @@ fn fetch_zls_version_data(
924961

925962
return version_data;
926963
}
964+
965+
/// Percent-encodes the unreserved subset needed for a Zig dev version pin
966+
/// (`0.17.0-dev.261+3d1fb4fac`) when used as a URL query value. The literal
967+
/// `+` would otherwise be decoded as a space by the server, so we encode it
968+
/// as `%2B`. All RFC 3986 unreserved characters and the dev-pin separators
969+
/// `.` and `-` are passed through; anything else is percent-encoded.
970+
fn percent_encode_query_value(input: []const u8, output_buffer: []u8) ![]const u8 {
971+
assert(input.len > 0);
972+
assert(output_buffer.len >= input.len * 3);
973+
974+
var write_index: usize = 0;
975+
for (input) |byte| {
976+
const passthrough = (byte >= 'A' and byte <= 'Z') or
977+
(byte >= 'a' and byte <= 'z') or
978+
(byte >= '0' and byte <= '9') or
979+
byte == '-' or byte == '.' or byte == '_' or byte == '~';
980+
if (passthrough) {
981+
output_buffer[write_index] = byte;
982+
write_index += 1;
983+
} else {
984+
const hex_digits = "0123456789ABCDEF";
985+
output_buffer[write_index] = '%';
986+
output_buffer[write_index + 1] = hex_digits[(byte >> 4) & 0x0f];
987+
output_buffer[write_index + 2] = hex_digits[byte & 0x0f];
988+
write_index += 3;
989+
}
990+
}
991+
assert(write_index >= input.len);
992+
assert(write_index <= output_buffer.len);
993+
return output_buffer[0..write_index];
994+
}
995+
996+
/// Translates the user-facing `master` alias into the concrete Zig dev
997+
/// version currently published in `index.json`. Pinned dev pins are copied
998+
/// through verbatim. The returned slice points into `output_buffer`.
999+
fn resolve_master_zig_version(
1000+
ctx: *context.CliContext,
1001+
version: []const u8,
1002+
output_buffer: *[limits.limits.version_string_length_maximum]u8,
1003+
) ![]const u8 {
1004+
assert(version.len > 0);
1005+
assert(util_tool.is_master_like_version(version));
1006+
1007+
if (util_tool.is_dev_version(version)) {
1008+
assert(version.len <= output_buffer.len);
1009+
@memcpy(output_buffer[0..version.len], version);
1010+
return output_buffer[0..version.len];
1011+
}
1012+
1013+
assert(util_tool.eql_str(version, "master"));
1014+
const res = try http_client.HttpClient.fetch(ctx, config.zig_url, .{});
1015+
assert(res.len > 0);
1016+
1017+
const resolved = try meta.Zig.get_master_version_string(res, output_buffer[0..]) orelse {
1018+
log.err("Zig index.json has no `master` entry; cannot resolve ZLS master build.", .{});
1019+
return error.UnsupportedVersion;
1020+
};
1021+
if (!util_tool.is_dev_version(resolved)) {
1022+
log.err("Zig master entry version '{s}' is not a dev pin.", .{resolved});
1023+
return error.UnsupportedVersion;
1024+
}
1025+
return resolved;
1026+
}
1027+
1028+
/// Fetches and parses the ZLS `select-version` payload for a master/dev
1029+
/// install. The endpoint requires a concrete Zig dev version as the
1030+
/// `zig_version` query parameter (the literal `master` is rejected with HTTP
1031+
/// 400), so the literal `master` alias is first resolved against Zig's
1032+
/// `index.json` to its current dev pin. Pinned dev versions are passed
1033+
/// through unchanged.
1034+
fn fetch_zls_master_version_data(
1035+
ctx: *context.CliContext,
1036+
platform_str: []const u8,
1037+
version: []const u8,
1038+
) !meta.Zls.MasterVersionData {
1039+
assert(platform_str.len > 0);
1040+
assert(version.len > 0);
1041+
assert(util_tool.is_master_like_version(version));
1042+
1043+
var resolved_buffer: [limits.limits.version_string_length_maximum]u8 = undefined;
1044+
const resolved_zig_version = try resolve_master_zig_version(ctx, version, &resolved_buffer);
1045+
assert(resolved_zig_version.len > 0);
1046+
assert(util_tool.is_dev_version(resolved_zig_version));
1047+
1048+
var encoded_version_buffer: [limits.limits.version_string_length_maximum * 3]u8 = undefined;
1049+
const encoded_zig_version = try percent_encode_query_value(
1050+
resolved_zig_version,
1051+
encoded_version_buffer[0..],
1052+
);
1053+
1054+
var url_buffer = try ctx.scratch(.path);
1055+
defer url_buffer.release();
1056+
const url = try url_buffer.set(try std.fmt.bufPrint(
1057+
url_buffer.slice(),
1058+
"{s}?zig_version={s}&compatibility=only-runtime",
1059+
.{ config.zls_select_version_url_base, encoded_zig_version },
1060+
));
1061+
assert(url.len > 0);
1062+
assert(url.len <= limits.limits.url_length_maximum);
1063+
1064+
const uri = std.Uri.parse(url) catch |err| {
1065+
log.err("Invalid ZLS select-version URL '{s}': {s}", .{ url, @errorName(err) });
1066+
return error.InvalidMetadataUrl;
1067+
};
1068+
1069+
const res = try http_client.HttpClient.fetch(ctx, uri, .{});
1070+
assert(res.len > 0);
1071+
1072+
const version_data = try meta.Zls.get_master_version_data(res, platform_str) orelse {
1073+
log.err("Unsupported ZLS master build for Zig '{s}' on platform '{s}'. The ZLS " ++
1074+
"select-version endpoint did not return an entry for this combination.", .{
1075+
version,
1076+
platform_str,
1077+
});
1078+
return error.UnsupportedVersion;
1079+
};
1080+
1081+
assert(version_data.version().len > 0);
1082+
assert(version_data.tarball().len > 0);
1083+
assert(version_data.size > 0);
1084+
1085+
return version_data;
1086+
}

src/core/meta/zig.zig

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,46 @@ pub const Zig = struct {
9494
return error.IndexEntryLimitExceeded;
9595
}
9696

97+
/// Reads the resolved dev version string from the `master` entry of the
98+
/// Zig `index.json` payload (e.g. `0.16.0-dev.418+abcdef0`). Used by the
99+
/// ZLS master install path to translate the user-facing alias `master`
100+
/// into a concrete Zig version that the ZLS `select-version` endpoint
101+
/// accepts.
102+
pub fn get_master_version_string(
103+
raw: []const u8,
104+
target_buffer: []u8,
105+
) !?[]const u8 {
106+
assert(raw.len > 0);
107+
assert(target_buffer.len > 0);
108+
109+
var scanner: TokenScanner = undefined;
110+
try scanner.init(raw);
111+
defer scanner.deinit();
112+
113+
try scanner.expect_object_begin();
114+
const entries_max: u32 = 4096;
115+
var entries_seen: u32 = 0;
116+
while (entries_seen < entries_max) : (entries_seen += 1) {
117+
switch (try scanner.peek_token_type()) {
118+
.object_end => {
119+
try scanner.expect_object_end();
120+
return null;
121+
},
122+
.string => {
123+
var key_buffer: [limits.limits.version_string_length_maximum]u8 = undefined;
124+
const key = try scanner.next_string(key_buffer[0..]);
125+
if (!util_tool.eql_str(key, "master")) {
126+
try scanner.skip_value();
127+
continue;
128+
}
129+
return try parse_master_version_field(&scanner, target_buffer);
130+
},
131+
else => return error.UnexpectedToken,
132+
}
133+
}
134+
return error.IndexEntryLimitExceeded;
135+
}
136+
97137
pub fn get_version_list(
98138
raw: []const u8,
99139
version_entries: []*object_pools.VersionEntry,
@@ -175,6 +215,33 @@ fn parse_version_entry(
175215
return version_data;
176216
}
177217

218+
fn parse_master_version_field(
219+
scanner: *TokenScanner,
220+
target_buffer: []u8,
221+
) !?[]const u8 {
222+
assert(target_buffer.len > 0);
223+
224+
try scanner.expect_object_begin();
225+
var version_text: ?[]const u8 = null;
226+
while (true) {
227+
switch (try scanner.peek_token_type()) {
228+
.object_end => break,
229+
.string => {
230+
var key_buffer: [limits.limits.path_length_maximum]u8 = undefined;
231+
const key = try scanner.next_string(key_buffer[0..]);
232+
if (util_tool.eql_str(key, "version")) {
233+
version_text = try scanner.next_string(target_buffer);
234+
} else {
235+
try scanner.skip_value();
236+
}
237+
},
238+
else => return error.UnexpectedToken,
239+
}
240+
}
241+
try scanner.expect_object_end();
242+
return version_text;
243+
}
244+
178245
fn parse_platform_entry(
179246
scanner: *TokenScanner,
180247
version_data: *Zig.VersionData,

0 commit comments

Comments
 (0)