From 22ab2e7611a61070318f0af6129783af428148ac Mon Sep 17 00:00:00 2001 From: Yasuo Honda Date: Thu, 4 Jun 2026 14:38:48 +0900 Subject: [PATCH 1/2] fix: auto-bootstrap root.json and create bin dir on first run When tiup is installed via mise (https://mise.jdx.dev, a polyglot tool version manager similar to asdf) with: mise install github:pingcap/tiup mise use -g github:pingcap/tiup the install.sh script is not executed, which means: 1. ~/.tiup/bin/ directory is not created 2. root.json is not downloaded from the mirror This leaves the installation incomplete and requires the user to manually run 'tiup mirror set' before any tiup command works. This patch fixes both issues: - ResetMirror now ensures the bin directory exists before writing root.json - InitEnv automatically bootstraps root.json from the default mirror if it's missing, so no manual 'tiup mirror set' is needed In the future, we would like to register tiup in the mise registry so that users can simply run: mise install tiup mise use tiup This fix is a prerequisite for that, as it removes the need for any post-install initialization steps. --- pkg/environment/env.go | 25 ++++++++++++++++++++----- pkg/localdata/profile.go | 5 +++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/pkg/environment/env.go b/pkg/environment/env.go index 8ea388cc3e..352f959a18 100644 --- a/pkg/environment/env.go +++ b/pkg/environment/env.go @@ -14,6 +14,7 @@ package environment import ( + "errors" "fmt" "os" "path/filepath" @@ -21,7 +22,7 @@ import ( "strings" "time" - "github.com/pingcap/errors" + perrs "github.com/pingcap/errors" "github.com/pingcap/tiup/pkg/localdata" "github.com/pingcap/tiup/pkg/repository" "github.com/pingcap/tiup/pkg/repository/v1manifest" @@ -144,7 +145,21 @@ func InitEnv(options repository.Options, mOpt repository.MirrorOptions) (*Enviro var local v1manifest.LocalManifests local, err = v1manifest.NewManifests(profile) if err != nil { - return nil, errors.Annotatef(err, "initial repository from mirror(%s) failed", mirrorAddr) + if errors.Is(err, v1manifest.ErrLoadManifest) { + // Only bootstrap root.json if the file is actually missing. + // If the file exists but is corrupted, preserve the original error. + rootPath := profile.Path("bin", "root.json") + if _, statErr := os.Stat(rootPath); os.IsNotExist(statErr) { + // Use the configured mirrorAddr so that custom/test mirrors are respected. + if err := profile.ResetMirror(mirrorAddr, ""); err != nil { + return nil, perrs.Annotatef(err, "initial repository from mirror(%s) failed", mirrorAddr) + } + local, err = v1manifest.NewManifests(profile) + } + } + if err != nil { + return nil, perrs.Annotatef(err, "initial repository from mirror(%s) failed", mirrorAddr) + } } v1repo = repository.NewV1Repo(mirror, options, local) @@ -222,7 +237,7 @@ func (env *Environment) SelectInstalledVersion(component string, ver utils.Versi versions := []string{} for _, v := range installed { vi, err := env.v1Repo.LocalComponentVersion(component, v, true) - if errors.Cause(err) == repository.ErrUnknownVersion { + if perrs.Cause(err) == repository.ErrUnknownVersion { continue } if err != nil { @@ -238,9 +253,9 @@ func (env *Environment) SelectInstalledVersion(component string, ver utils.Versi return semver.Compare(versions[i], versions[j]) > 0 }) - errInstallFirst := errors.Annotatef(ErrInstallFirst, "use `tiup install %s` to install component `%s` first", component, component) + errInstallFirst := perrs.Annotatef(ErrInstallFirst, "use `tiup install %s` to install component `%s` first", component, component) if !ver.IsEmpty() { - errInstallFirst = errors.Annotatef(ErrInstallFirst, "use `tiup install %s:%s` to install specified version", component, ver.String()) + errInstallFirst = perrs.Annotatef(ErrInstallFirst, "use `tiup install %s:%s` to install specified version", component, ver.String()) } if ver.IsEmpty() || string(ver) == utils.NightlyVersionAlias { diff --git a/pkg/localdata/profile.go b/pkg/localdata/profile.go index 1712536fb5..0d29550c01 100644 --- a/pkg/localdata/profile.go +++ b/pkg/localdata/profile.go @@ -224,6 +224,11 @@ func (p *Profile) VersionIsInstalled(component, version string) (bool, error) { // ResetMirror reset root.json and cleanup manifests directory func (p *Profile) ResetMirror(addr, root string) error { + // Ensure bin directory exists + if err := os.MkdirAll(p.Path("bin"), 0755); err != nil { + return err + } + // Calculating root.json path shaWriter := sha256.New() if _, err := io.Copy(shaWriter, strings.NewReader(addr)); err != nil { From 86ccffc557a598842a93f71fab2c777f6d83faeb Mon Sep 17 00:00:00 2001 From: Yasuo Honda Date: Thu, 4 Jun 2026 16:46:43 +0900 Subject: [PATCH 2/2] env: keep ErrInstallFirst as a pingcap error and clarify bootstrap comment After aliasing the pingcap/errors import to perrs (to free the name for the stdlib errors used by errors.Is), the package-level ErrInstallFirst sentinel silently switched to stdlib errors.New. Use perrs.New to preserve the prior behavior. Also reword the bootstrap comment: a present-but-corrupt root.json never yields ErrLoadManifest, so the os.Stat guard actually covers the exists-but-unreadable case. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/environment/env.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/environment/env.go b/pkg/environment/env.go index 352f959a18..c88150823a 100644 --- a/pkg/environment/env.go +++ b/pkg/environment/env.go @@ -33,7 +33,7 @@ import ( var ( // ErrInstallFirst indicates that a component/version is not installed - ErrInstallFirst = errors.New("component not installed") + ErrInstallFirst = perrs.New("component not installed") ) // EnvList is the canonical allowlist of environment variables TiUP will print or expose. @@ -146,8 +146,9 @@ func InitEnv(options repository.Options, mOpt repository.MirrorOptions) (*Enviro local, err = v1manifest.NewManifests(profile) if err != nil { if errors.Is(err, v1manifest.ErrLoadManifest) { - // Only bootstrap root.json if the file is actually missing. - // If the file exists but is corrupted, preserve the original error. + // Only bootstrap root.json when the file is actually missing. + // If it exists but can't be read (e.g. permissions), preserve the + // original error rather than overwriting it. rootPath := profile.Path("bin", "root.json") if _, statErr := os.Stat(rootPath); os.IsNotExist(statErr) { // Use the configured mirrorAddr so that custom/test mirrors are respected.