@@ -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+
399436fn 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+ }
0 commit comments