From 7b4ec20fb73bfd788803705ffc103fc942958271 Mon Sep 17 00:00:00 2001 From: no-realm <7832423+no-realm@users.noreply.github.com> Date: Wed, 23 Oct 2024 02:34:43 +0200 Subject: [PATCH] switch to using the zls release worker for selecting compatible zls versions based on the installed zig version --- README.md | 14 +++ cli/error.go | 6 +- cli/install.go | 280 ++++++++++++++++++++++--------------------------- cli/ls.go | 2 +- cli/version.go | 9 +- main.go | 14 ++- 6 files changed, 161 insertions(+), 164 deletions(-) diff --git a/README.md b/README.md index 6d9c97d..f31038f 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,20 @@ pass the `--zls` flag with `zvm i`. For example: zvm i --zls master ``` +#### Select ZLS compatibility mode + +By default, ZVM will install a ZLS build, which can be used with the given Zig version, +but may not be able to build ZLS from source. +If you want to use a ZLS build, which can be built using the selected Zig version, pass +the `--full` flag with `zvm i --zls`. For example: + +```sh +zvm i --zls --full master +``` + +> [!IMPORTANT] +> This does not apply to tagged releases, e.g.: `0.13.0` + ## Switch between installed Zig versions ```sh diff --git a/cli/error.go b/cli/error.go index 66bac89..67ada75 100644 --- a/cli/error.go +++ b/cli/error.go @@ -17,6 +17,8 @@ var ( ErrInvalidVersionMap = errors.New("invalid version map format") ErrInvalidInput = errors.New("invalid input") // ErrDownloadFail is an an error when fetching Zig, or constructing a target URL to fetch Zig. - ErrDownloadFail = errors.New("failed to download Zig") - ErrNoZlsVersion = errors.New("zls release worker returned error") + ErrDownloadFail = errors.New("failed to download Zig") + ErrNoZlsVersion = errors.New("zls release worker returned error") + ErrMissingVersionInfo = errors.New("version info not found") + ErrMissingShasum = errors.New("shasum not found") ) diff --git a/cli/install.go b/cli/install.go index f6695af..1305223 100644 --- a/cli/install.go +++ b/cli/install.go @@ -6,10 +6,8 @@ package cli import ( "archive/zip" - "bytes" "crypto/sha256" "encoding/hex" - "encoding/json" "errors" "fmt" "io" @@ -287,233 +285,201 @@ func mirrorMachEngine(url string) (string, error) { return strings.Replace(url, "https://ziglang.org/builds/", "https://pkg.machengine.org/zig/", 1), nil } -type githubTaggedReleaseResponse struct { - Assets []gitHubAsset // json array of platform binaries -} - -type gitHubAsset struct { - Url string // url for asset json object - Name string // contains platform information about binary - BrowserDownloadUrl string `json:"browser_download_url"` // download url -} - -type zlsCIDownloadIndexResponse struct { - Versions map[string]zlsCIZLSVersion - Latest string // most recent ZLS version - LatestTagged string // most recent tagged ZLS version -} - -type zlsCIZLSVersion struct { - ZLSVersion string - Targets []string -} - -func getZLSDownloadUrl(version string, archDouble string) (string, string, error) { - if version == "master" { - resp, err := http.Get("https://zigtools-releases.nyc3.digitaloceanspaces.com/zls/index.json") - if err != nil { - return "", "", err - } - defer resp.Body.Close() - - var releaseBuffer bytes.Buffer - _, err = releaseBuffer.ReadFrom(resp.Body) - if err != nil { - return "", "", err - } - - var ciIndex zlsCIDownloadIndexResponse - if err := json.Unmarshal(releaseBuffer.Bytes(), &ciIndex); err != nil { - return "", "", err - } - - exeName := "zls" - if strings.Contains(archDouble, "windows") { - exeName = "zls.exe" - } - - format_url := "https://zigtools-releases.nyc3.digitaloceanspaces.com/zls/%v/%v/%v" - return fmt.Sprintf(format_url, ciIndex.Latest, archDouble, exeName), ciIndex.Latest, nil - } else { - url := fmt.Sprintf("https://api.github.com/repos/zigtools/zls/releases/tags/%v", version) +func (z *ZVM) SelectZlsVersion(version string, compatMode string) (string, string, string, error) { + rawVersionStructure, err := z.fetchZlsTaggedVersionMap() + if err != nil { + return "", "", "", err + } - // get release information - resp, err := http.Get(url) - if err != nil { - return "", "", err + // tagged releases. + tarPath, err := getTarPath(version, &rawVersionStructure) + if err == nil { + shasum, err := getVersionShasum(version, &rawVersionStructure) + if err == nil { + return version, tarPath, shasum, nil } - defer resp.Body.Close() + } - var releaseBuffer bytes.Buffer - _, err = releaseBuffer.ReadFrom(resp.Body) + // master/nightly releases. + if err == ErrUnsupportedVersion { + info, err := z.fetchZlsVersionByZigVersion(version, compatMode) if err != nil { - return "", "", err + return "", "", "", err } - // getting list of assets - var taggedReleaseResponse githubTaggedReleaseResponse - if err := json.Unmarshal(releaseBuffer.Bytes(), &taggedReleaseResponse); err != nil { - return "", "", err + zlsVersion, ok := info["version"].(string) + if !ok { + return "", "", "", ErrMissingVersionInfo } - if len(taggedReleaseResponse.Assets) == 0 { - return "", "", errors.New("invalid ZLS version") + arch, ops := zigStyleSysInfo() + systemInfo, ok := info[fmt.Sprintf("%s-%s", arch, ops)].(map[string]any) + if !ok { + return "", "", "", ErrUnsupportedSystem } - // getting platform information - var downloadUrl string - for _, asset := range taggedReleaseResponse.Assets { - if strings.Contains(asset.Name, archDouble) { - downloadUrl = asset.BrowserDownloadUrl - break - } + tar, ok := systemInfo["tarball"].(string) + if !ok { + return "", "", "", ErrMissingBundlePath } - if downloadUrl == "" { - return "", "", errors.New("invalid ZLS release URL") + shasum, ok := systemInfo["shasum"].(string) + if !ok { + return "", "", "", ErrMissingShasum } - return downloadUrl, version, nil + return zlsVersion, tar, shasum, nil } -} -func (z *ZVM) InstallZls(version string, force bool) error { - if version != "master" && strings.Count(version, ".") != 2 { - return fmt.Errorf("%w: versions are SEMVER (MAJOR.MINOR.MINUSCULE)", ErrUnsupportedVersion) - } + return "", "", "", err +} - fmt.Println("Finding ZLS executable...") +func (z *ZVM) InstallZls(requestedVersion string, compatMode string, force bool) error { + fmt.Println("Determining installed Zig version...") // make sure dir exists - installDir := filepath.Join(z.baseDir, version) + installDir := filepath.Join(z.baseDir, requestedVersion) err := os.MkdirAll(installDir, 0755) if err != nil { return err } - arch, osType := zigStyleSysInfo() - expectedArchOs := fmt.Sprintf("%v-%v", arch, osType) - - filename := "zls" - if osType == "windows" { - filename += ".exe" + targetZig := strings.TrimSpace(filepath.Join(z.baseDir, requestedVersion, "zig")) + cmd := exec.Command(targetZig, "version") + var builder strings.Builder + cmd.Stdout = &builder + err = cmd.Run() + if err != nil { + log.Warn(err) } + zigVersion := strings.TrimSpace(builder.String()) + log.Debug("installed zig version", "version", zigVersion) - // master does not need unzipping, zpm just serves full binary - shouldUnzip := version != "master" + fmt.Println("Selecting ZLS version...") - downloadUrl, selectedVersion, err := getZLSDownloadUrl(version, expectedArchOs) + zlsVersion, tarPath, shasum, err := z.SelectZlsVersion(zigVersion, compatMode) if err != nil { - return err + if errors.Is(err, ErrUnsupportedVersion) { + return fmt.Errorf("%s: %q", err, zigVersion) + } else { + return err + } + } + log.Debug("selected zls version", "zigVersion", zigVersion, "zlsVersion", zlsVersion) + + _, osType := zigStyleSysInfo() + filename := "zls" + if osType == "windows" { + filename += ".exe" } if !force { installedVersion := "" - targetZls := strings.TrimSpace(filepath.Join(z.baseDir, version, "zls")) + targetZls := strings.TrimSpace(filepath.Join(installDir, filename)) if _, err := os.Stat(targetZls); err == nil { cmd := exec.Command(targetZls, "--version") - var zigVersion strings.Builder - cmd.Stdout = &zigVersion + var builder strings.Builder + cmd.Stdout = &builder err := cmd.Run() if err != nil { log.Warn(err) } - installedVersion = strings.TrimSpace(zigVersion.String()) + installedVersion = strings.TrimSpace(builder.String()) } - if installedVersion == selectedVersion { + if installedVersion == zlsVersion { fmt.Printf("ZLS version %s is already installed\n", installedVersion) return nil } } - request, err := http.NewRequest("GET", downloadUrl, nil) + log.Debug("tarPath", "url", tarPath) + + tarResp, err := reqZigDownload(tarPath) if err != nil { return err } + defer tarResp.Body.Close() - request.Header.Set("User-Agent", "zvm "+meta.VERSION) + var pathEnding string + if runtime.GOOS == "windows" { + pathEnding = "*.zip" + } else { + pathEnding = "*.tar.xz" + } - response, err := http.DefaultClient.Do(request) + tempDir, err := os.CreateTemp(z.baseDir, pathEnding) if err != nil { return err } - defer response.Body.Close() - // if resp.ContentLength == 0 { - // return fmt.Errorf("invalid ZLS content length (%d bytes)", resp.ContentLength) - // } + defer tempDir.Close() + defer os.RemoveAll(tempDir.Name()) + + var clr_opt_ver_str string + if z.Settings.UseColor { + clr_opt_ver_str = clr.Green(zigVersion) + } else { + clr_opt_ver_str = zigVersion + } pbar := progressbar.DefaultBytes( - int64(response.ContentLength), - "Downloading ZLS", + int64(tarResp.ContentLength), + fmt.Sprintf("Downloading %s:", clr_opt_ver_str), ) - versionPath := filepath.Join(z.baseDir, version) - binaryLocation := filepath.Join(versionPath, filename) - - if !shouldUnzip { - file, err := os.Create(binaryLocation) - if err != nil { - return err - } - defer file.Close() + hash := sha256.New() + _, err = io.Copy(io.MultiWriter(tempDir, pbar, hash), tarResp.Body) + if err != nil { + return err + } - if _, err := io.Copy(io.MultiWriter(pbar, file), response.Body); err != nil { - return err + fmt.Println("Checking ZLS shasum...") + if len(shasum) > 0 { + ourHexHash := hex.EncodeToString(hash.Sum(nil)) + log.Debug("shasum check:", "theirs", shasum, "ours", ourHexHash) + if ourHexHash != shasum { + // TODO (tristan) + // Why is my sha256 identical on the server and sha256sum, + // but not when I download it in ZVM? Oh shit. + // It's because it's a compressed download. + return fmt.Errorf("shasum for zls-%v does not match expected value", zlsVersion) } + fmt.Println("Shasums for ZLS match! 🎉") } else { - var pathEnding string - if runtime.GOOS == "windows" { - pathEnding = "*.zip" - } else { - pathEnding = "*.tar.xz" - } - - tempFile, err := os.CreateTemp(z.baseDir, pathEnding) - if err != nil { - return err - } - - defer tempFile.Close() - defer os.RemoveAll(tempFile.Name()) - - if _, err := io.Copy(io.MultiWriter(pbar, tempFile), response.Body); err != nil { - return err - } - - zlsTempDir, err := os.MkdirTemp(z.baseDir, "zls-*") - if err != nil { - return err - } + log.Warnf("No ZLS shasum provided by host") + } - defer os.RemoveAll(zlsTempDir) + fmt.Println("Extracting ZLS bundle...") - fmt.Println("Extracting ZLS...") // Edgy bit - if err := ExtractBundle(tempFile.Name(), zlsTempDir); err != nil { - log.Fatal(err) - } + zlsTempDir, err := os.MkdirTemp(z.baseDir, "zls-*") + if err != nil { + return err + } + defer os.RemoveAll(zlsTempDir) - zlsPath, err := findZlsExecutable(zlsTempDir) - if err != nil { - return err - } + if err := ExtractBundle(tempDir.Name(), zlsTempDir); err != nil { + log.Fatal(err) + } - if err := os.Rename(zlsPath, filepath.Join(versionPath, filename)); err != nil { - return err - } + zlsPath, err := findZlsExecutable(zlsTempDir) + if err != nil { + return err + } - if zlsPath == "" { - return fmt.Errorf("could not find ZLS in %q", zlsTempDir) - } + if err := os.Rename(zlsPath, filepath.Join(installDir, filename)); err != nil { + return err + } + if zlsPath == "" { + return fmt.Errorf("could not find ZLS in %q", zlsTempDir) } - if err := os.Chmod(filepath.Join(versionPath, filename), 0755); err != nil { + if err := os.Chmod(filepath.Join(installDir, filename), 0755); err != nil { return err } - z.createSymlink(version) + z.createSymlink(requestedVersion) fmt.Println("Done! 🎉") return nil } diff --git a/cli/ls.go b/cli/ls.go index 8702298..0ebb983 100644 --- a/cli/ls.go +++ b/cli/ls.go @@ -60,7 +60,7 @@ func (z *ZVM) GetInstalledVersions() ([]string, error) { versions := make([]string, 0, len(dir)) for _, key := range dir { switch key.Name() { - case "settings.json", "bin", "versions.json", "self": + case "settings.json", "bin", "versions.json", "versions-zls.json", "self": continue default: versions = append(versions, key.Name()) diff --git a/cli/version.go b/cli/version.go index 15b5f5b..5d95db9 100644 --- a/cli/version.go +++ b/cli/version.go @@ -130,7 +130,7 @@ func (z *ZVM) fetchZlsTaggedVersionMap() (zigVersionMap, error) { // note: the zls release-worker uses the same index format as zig, but without the latest master entry. // this function does not write the result to a file. -func (z *ZVM) fetchZlsVersionByZigVersion(version string) (zigVersion, error) { +func (z *ZVM) fetchZlsVersionByZigVersion(version string, compatMode string) (zigVersion, error) { log.Debug("inital ZRW", "url", z.Settings.ZlsReleaseWorkerBaseUrl) if err := z.loadSettings(); err != nil { @@ -152,7 +152,8 @@ func (z *ZVM) fetchZlsVersionByZigVersion(version string) (zigVersion, error) { // The compatibility query parameter must be either only-runtime or full: // full: Request a ZLS build that can be built and used with the given Zig version. // only-runtime: Request a ZLS build that can be used at runtime with the given Zig version but may not be able to build ZLS from source. - selectVersionUrl := fmt.Sprintf("%s/v1/zls/select-version?zig_version=%s&compatibility=full", zrwBaseUrl, url.QueryEscape(version)) + selectVersionUrl := fmt.Sprintf("%s/v1/zls/select-version?zig_version=%s&compatibility=%s", zrwBaseUrl, url.QueryEscape(version), compatMode) + log.Debug("fetching zls version", "zigVersion", version, "url", selectVersionUrl) req, err := http.NewRequest("GET", selectVersionUrl, nil) if err != nil { return nil, err @@ -181,6 +182,10 @@ func (z *ZVM) fetchZlsVersionByZigVersion(version string) (zigVersion, error) { return nil, err } + if badRequest, ok := rawVersionStructure["error"].(string); ok { + return nil, fmt.Errorf("%w: %s", ErrNoZlsVersion, badRequest) + } + if code, ok := rawVersionStructure["code"]; ok { codeStr := strconv.FormatFloat(code.(float64), 'f', 0, 64) msg := rawVersionStructure["message"] diff --git a/main.go b/main.go index 40b64c5..e440286 100644 --- a/main.go +++ b/main.go @@ -71,6 +71,10 @@ var zvmApp = &opts.App{ Aliases: []string{"f"}, Usage: "force installation even if the version is already installed", }, + &opts.BoolFlag{ + Name: "full", + Usage: "use the 'full' zls compatibility mode", + }, }, Description: "To install the latest version, use `master`", Args: true, @@ -91,14 +95,20 @@ var zvmApp = &opts.App{ force = ctx.Bool("force") } + zlsCompat := "only-runtime" + if ctx.Bool("full") { + zlsCompat = "full" + } + // Install Zig - if err := zvm.Install(req.Package, force); err != nil { + err := zvm.Install(req.Package, force) + if err != nil { return err } // Install ZLS (if requested) if ctx.Bool("zls") { - if err := zvm.InstallZls(req.Package, force); err != nil { + if err := zvm.InstallZls(req.Package, zlsCompat, force); err != nil { return err } }