github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/extension/manager.go (about) 1 package extension 2 3 import ( 4 "bytes" 5 "context" 6 _ "embed" 7 "errors" 8 "fmt" 9 "io" 10 "io/fs" 11 "net/http" 12 "os" 13 "os/exec" 14 "path" 15 "path/filepath" 16 "runtime" 17 "strings" 18 "sync" 19 20 "github.com/ungtb10d/cli/v2/api" 21 "github.com/ungtb10d/cli/v2/git" 22 "github.com/ungtb10d/cli/v2/internal/config" 23 "github.com/ungtb10d/cli/v2/internal/ghrepo" 24 "github.com/ungtb10d/cli/v2/pkg/extensions" 25 "github.com/ungtb10d/cli/v2/pkg/findsh" 26 "github.com/ungtb10d/cli/v2/pkg/iostreams" 27 "github.com/cli/safeexec" 28 "gopkg.in/yaml.v3" 29 ) 30 31 type Manager struct { 32 dataDir func() string 33 lookPath func(string) (string, error) 34 findSh func() (string, error) 35 newCommand func(string, ...string) *exec.Cmd 36 platform func() (string, string) 37 client *http.Client 38 config config.Config 39 io *iostreams.IOStreams 40 dryRunMode bool 41 } 42 43 func NewManager(ios *iostreams.IOStreams) *Manager { 44 return &Manager{ 45 dataDir: config.DataDir, 46 lookPath: safeexec.LookPath, 47 findSh: findsh.Find, 48 newCommand: exec.Command, 49 platform: func() (string, string) { 50 ext := "" 51 if runtime.GOOS == "windows" { 52 ext = ".exe" 53 } 54 return fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH), ext 55 }, 56 io: ios, 57 } 58 } 59 60 func (m *Manager) SetConfig(cfg config.Config) { 61 m.config = cfg 62 } 63 64 func (m *Manager) SetClient(client *http.Client) { 65 m.client = client 66 } 67 68 func (m *Manager) EnableDryRunMode() { 69 m.dryRunMode = true 70 } 71 72 func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) { 73 if len(args) == 0 { 74 return false, errors.New("too few arguments in list") 75 } 76 77 var exe string 78 extName := args[0] 79 forwardArgs := args[1:] 80 81 exts, _ := m.list(false) 82 var ext Extension 83 for _, e := range exts { 84 if e.Name() == extName { 85 ext = e 86 exe = ext.Path() 87 break 88 } 89 } 90 if exe == "" { 91 return false, nil 92 } 93 94 var externalCmd *exec.Cmd 95 96 if ext.IsBinary() || runtime.GOOS != "windows" { 97 externalCmd = m.newCommand(exe, forwardArgs...) 98 } else if runtime.GOOS == "windows" { 99 // Dispatch all extension calls through the `sh` interpreter to support executable files with a 100 // shebang line on Windows. 101 shExe, err := m.findSh() 102 if err != nil { 103 if errors.Is(err, exec.ErrNotFound) { 104 return true, errors.New("the `sh.exe` interpreter is required. Please install Git for Windows and try again") 105 } 106 return true, err 107 } 108 forwardArgs = append([]string{"-c", `command "$@"`, "--", exe}, forwardArgs...) 109 externalCmd = m.newCommand(shExe, forwardArgs...) 110 } 111 externalCmd.Stdin = stdin 112 externalCmd.Stdout = stdout 113 externalCmd.Stderr = stderr 114 return true, externalCmd.Run() 115 } 116 117 func (m *Manager) List() []extensions.Extension { 118 exts, _ := m.list(false) 119 r := make([]extensions.Extension, len(exts)) 120 for i, v := range exts { 121 val := v 122 r[i] = &val 123 } 124 return r 125 } 126 127 func (m *Manager) list(includeMetadata bool) ([]Extension, error) { 128 dir := m.installDir() 129 entries, err := os.ReadDir(dir) 130 if err != nil { 131 return nil, err 132 } 133 134 var results []Extension 135 for _, f := range entries { 136 if !strings.HasPrefix(f.Name(), "gh-") { 137 continue 138 } 139 var ext Extension 140 var err error 141 if f.IsDir() { 142 ext, err = m.parseExtensionDir(f) 143 if err != nil { 144 return nil, err 145 } 146 results = append(results, ext) 147 } else { 148 ext, err = m.parseExtensionFile(f) 149 if err != nil { 150 return nil, err 151 } 152 results = append(results, ext) 153 } 154 } 155 156 if includeMetadata { 157 m.populateLatestVersions(results) 158 } 159 160 return results, nil 161 } 162 163 func (m *Manager) parseExtensionFile(fi fs.DirEntry) (Extension, error) { 164 ext := Extension{isLocal: true} 165 id := m.installDir() 166 exePath := filepath.Join(id, fi.Name(), fi.Name()) 167 if !isSymlink(fi.Type()) { 168 // if this is a regular file, its contents is the local directory of the extension 169 p, err := readPathFromFile(filepath.Join(id, fi.Name())) 170 if err != nil { 171 return ext, err 172 } 173 exePath = filepath.Join(p, fi.Name()) 174 } 175 ext.path = exePath 176 return ext, nil 177 } 178 179 func (m *Manager) parseExtensionDir(fi fs.DirEntry) (Extension, error) { 180 id := m.installDir() 181 if _, err := os.Stat(filepath.Join(id, fi.Name(), manifestName)); err == nil { 182 return m.parseBinaryExtensionDir(fi) 183 } 184 185 return m.parseGitExtensionDir(fi) 186 } 187 188 func (m *Manager) parseBinaryExtensionDir(fi fs.DirEntry) (Extension, error) { 189 id := m.installDir() 190 exePath := filepath.Join(id, fi.Name(), fi.Name()) 191 ext := Extension{path: exePath, kind: BinaryKind} 192 manifestPath := filepath.Join(id, fi.Name(), manifestName) 193 manifest, err := os.ReadFile(manifestPath) 194 if err != nil { 195 return ext, fmt.Errorf("could not open %s for reading: %w", manifestPath, err) 196 } 197 var bm binManifest 198 err = yaml.Unmarshal(manifest, &bm) 199 if err != nil { 200 return ext, fmt.Errorf("could not parse %s: %w", manifestPath, err) 201 } 202 repo := ghrepo.NewWithHost(bm.Owner, bm.Name, bm.Host) 203 remoteURL := ghrepo.GenerateRepoURL(repo, "") 204 ext.url = remoteURL 205 ext.currentVersion = bm.Tag 206 ext.isPinned = bm.IsPinned 207 return ext, nil 208 } 209 210 func (m *Manager) parseGitExtensionDir(fi fs.DirEntry) (Extension, error) { 211 id := m.installDir() 212 exePath := filepath.Join(id, fi.Name(), fi.Name()) 213 remoteUrl := m.getRemoteUrl(fi.Name()) 214 currentVersion := m.getCurrentVersion(fi.Name()) 215 216 var isPinned bool 217 pinPath := filepath.Join(id, fi.Name(), fmt.Sprintf(".pin-%s", currentVersion)) 218 if _, err := os.Stat(pinPath); err == nil { 219 isPinned = true 220 } 221 222 return Extension{ 223 path: exePath, 224 url: remoteUrl, 225 isLocal: false, 226 currentVersion: currentVersion, 227 kind: GitKind, 228 isPinned: isPinned, 229 }, nil 230 } 231 232 // getCurrentVersion determines the current version for non-local git extensions. 233 func (m *Manager) getCurrentVersion(extension string) string { 234 gitExe, err := m.lookPath("git") 235 if err != nil { 236 return "" 237 } 238 dir := m.installDir() 239 gitDir := "--git-dir=" + filepath.Join(dir, extension, ".git") 240 cmd := m.newCommand(gitExe, gitDir, "rev-parse", "HEAD") 241 242 localSha, err := cmd.Output() 243 if err != nil { 244 return "" 245 } 246 return string(bytes.TrimSpace(localSha)) 247 } 248 249 // getRemoteUrl determines the remote URL for non-local git extensions. 250 func (m *Manager) getRemoteUrl(extension string) string { 251 gitExe, err := m.lookPath("git") 252 if err != nil { 253 return "" 254 } 255 dir := m.installDir() 256 gitDir := "--git-dir=" + filepath.Join(dir, extension, ".git") 257 cmd := m.newCommand(gitExe, gitDir, "config", "remote.origin.url") 258 url, err := cmd.Output() 259 if err != nil { 260 return "" 261 } 262 return strings.TrimSpace(string(url)) 263 } 264 265 func (m *Manager) populateLatestVersions(exts []Extension) { 266 size := len(exts) 267 type result struct { 268 index int 269 version string 270 } 271 ch := make(chan result, size) 272 var wg sync.WaitGroup 273 wg.Add(size) 274 for idx, ext := range exts { 275 go func(i int, e Extension) { 276 defer wg.Done() 277 version, _ := m.getLatestVersion(e) 278 ch <- result{index: i, version: version} 279 }(idx, ext) 280 } 281 wg.Wait() 282 close(ch) 283 for r := range ch { 284 ext := &exts[r.index] 285 ext.latestVersion = r.version 286 } 287 } 288 289 func (m *Manager) getLatestVersion(ext Extension) (string, error) { 290 if ext.isLocal { 291 return "", localExtensionUpgradeError 292 } 293 if ext.IsBinary() { 294 repo, err := ghrepo.FromFullName(ext.url) 295 if err != nil { 296 return "", err 297 } 298 r, err := fetchLatestRelease(m.client, repo) 299 if err != nil { 300 return "", err 301 } 302 return r.Tag, nil 303 } else { 304 gitExe, err := m.lookPath("git") 305 if err != nil { 306 return "", err 307 } 308 extDir := filepath.Dir(ext.path) 309 gitDir := "--git-dir=" + filepath.Join(extDir, ".git") 310 cmd := m.newCommand(gitExe, gitDir, "ls-remote", "origin", "HEAD") 311 lsRemote, err := cmd.Output() 312 if err != nil { 313 return "", err 314 } 315 remoteSha := bytes.SplitN(lsRemote, []byte("\t"), 2)[0] 316 return string(remoteSha), nil 317 } 318 } 319 320 func (m *Manager) InstallLocal(dir string) error { 321 name := filepath.Base(dir) 322 targetLink := filepath.Join(m.installDir(), name) 323 if err := os.MkdirAll(filepath.Dir(targetLink), 0755); err != nil { 324 return err 325 } 326 return makeSymlink(dir, targetLink) 327 } 328 329 type binManifest struct { 330 Owner string 331 Name string 332 Host string 333 Tag string 334 IsPinned bool 335 // TODO I may end up not using this; just thinking ahead to local installs 336 Path string 337 } 338 339 // Install installs an extension from repo, and pins to commitish if provided 340 func (m *Manager) Install(repo ghrepo.Interface, target string) error { 341 isBin, err := isBinExtension(m.client, repo) 342 if err != nil { 343 if errors.Is(err, releaseNotFoundErr) { 344 if ok, err := repoExists(m.client, repo); err != nil { 345 return err 346 } else if !ok { 347 return repositoryNotFoundErr 348 } 349 } else { 350 return fmt.Errorf("could not check for binary extension: %w", err) 351 } 352 } 353 if isBin { 354 return m.installBin(repo, target) 355 } 356 357 hs, err := hasScript(m.client, repo) 358 if err != nil { 359 return err 360 } 361 if !hs { 362 return errors.New("extension is not installable: missing executable") 363 } 364 365 return m.installGit(repo, target, m.io.Out, m.io.ErrOut) 366 } 367 368 func (m *Manager) installBin(repo ghrepo.Interface, target string) error { 369 var r *release 370 var err error 371 isPinned := target != "" 372 if isPinned { 373 r, err = fetchReleaseFromTag(m.client, repo, target) 374 } else { 375 r, err = fetchLatestRelease(m.client, repo) 376 } 377 if err != nil { 378 return err 379 } 380 381 platform, ext := m.platform() 382 isMacARM := platform == "darwin-arm64" 383 trueARMBinary := false 384 385 var asset *releaseAsset 386 for _, a := range r.Assets { 387 if strings.HasSuffix(a.Name, platform+ext) { 388 asset = &a 389 trueARMBinary = isMacARM 390 break 391 } 392 } 393 394 // if an arm64 binary is unavailable, fall back to amd64 if it can be executed through Rosetta 2 395 if asset == nil && isMacARM && hasRosetta() { 396 for _, a := range r.Assets { 397 if strings.HasSuffix(a.Name, "darwin-amd64") { 398 asset = &a 399 break 400 } 401 } 402 } 403 404 if asset == nil { 405 return fmt.Errorf( 406 "%[1]s unsupported for %[2]s. Open an issue: `gh issue create -R %[3]s/%[1]s -t'Support %[2]s'`", 407 repo.RepoName(), platform, repo.RepoOwner()) 408 } 409 410 name := repo.RepoName() 411 targetDir := filepath.Join(m.installDir(), name) 412 413 // TODO clean this up if function errs? 414 if !m.dryRunMode { 415 err = os.MkdirAll(targetDir, 0755) 416 if err != nil { 417 return fmt.Errorf("failed to create installation directory: %w", err) 418 } 419 } 420 421 binPath := filepath.Join(targetDir, name) 422 binPath += ext 423 424 if !m.dryRunMode { 425 err = downloadAsset(m.client, *asset, binPath) 426 if err != nil { 427 return fmt.Errorf("failed to download asset %s: %w", asset.Name, err) 428 } 429 if trueARMBinary { 430 if err := codesignBinary(binPath); err != nil { 431 return fmt.Errorf("failed to codesign downloaded binary: %w", err) 432 } 433 } 434 } 435 436 manifest := binManifest{ 437 Name: name, 438 Owner: repo.RepoOwner(), 439 Host: repo.RepoHost(), 440 Path: binPath, 441 Tag: r.Tag, 442 IsPinned: isPinned, 443 } 444 445 bs, err := yaml.Marshal(manifest) 446 if err != nil { 447 return fmt.Errorf("failed to serialize manifest: %w", err) 448 } 449 450 if !m.dryRunMode { 451 manifestPath := filepath.Join(targetDir, manifestName) 452 453 f, err := os.OpenFile(manifestPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 454 if err != nil { 455 return fmt.Errorf("failed to open manifest for writing: %w", err) 456 } 457 defer f.Close() 458 459 _, err = f.Write(bs) 460 if err != nil { 461 return fmt.Errorf("failed write manifest file: %w", err) 462 } 463 } 464 465 return nil 466 } 467 468 func (m *Manager) installGit(repo ghrepo.Interface, target string, stdout, stderr io.Writer) error { 469 protocol, _ := m.config.GetOrDefault(repo.RepoHost(), "git_protocol") 470 cloneURL := ghrepo.FormatRemoteURL(repo, protocol) 471 472 exe, err := m.lookPath("git") 473 if err != nil { 474 return err 475 } 476 477 var commitSHA string 478 if target != "" { 479 commitSHA, err = fetchCommitSHA(m.client, repo, target) 480 if err != nil { 481 return err 482 } 483 } 484 485 name := strings.TrimSuffix(path.Base(cloneURL), ".git") 486 targetDir := filepath.Join(m.installDir(), name) 487 488 externalCmd := m.newCommand(exe, "clone", cloneURL, targetDir) 489 externalCmd.Stdout = stdout 490 externalCmd.Stderr = stderr 491 if err := externalCmd.Run(); err != nil { 492 return err 493 } 494 if commitSHA == "" { 495 return nil 496 } 497 498 checkoutCmd := m.newCommand(exe, "-C", targetDir, "checkout", commitSHA) 499 checkoutCmd.Stdout = stdout 500 checkoutCmd.Stderr = stderr 501 if err := checkoutCmd.Run(); err != nil { 502 return err 503 } 504 505 pinPath := filepath.Join(targetDir, fmt.Sprintf(".pin-%s", commitSHA)) 506 f, err := os.OpenFile(pinPath, os.O_WRONLY|os.O_CREATE, 0600) 507 if err != nil { 508 return fmt.Errorf("failed to create pin file in directory: %w", err) 509 } 510 return f.Close() 511 } 512 513 var pinnedExtensionUpgradeError = errors.New("pinned extensions can not be upgraded") 514 var localExtensionUpgradeError = errors.New("local extensions can not be upgraded") 515 var upToDateError = errors.New("already up to date") 516 var noExtensionsInstalledError = errors.New("no extensions installed") 517 518 func (m *Manager) Upgrade(name string, force bool) error { 519 // Fetch metadata during list only when upgrading all extensions. 520 // This is a performance improvement so that we don't make a 521 // bunch of unnecessary network requests when trying to upgrade a single extension. 522 fetchMetadata := name == "" 523 exts, _ := m.list(fetchMetadata) 524 if len(exts) == 0 { 525 return noExtensionsInstalledError 526 } 527 if name == "" { 528 return m.upgradeExtensions(exts, force) 529 } 530 for _, f := range exts { 531 if f.Name() != name { 532 continue 533 } 534 var err error 535 // For single extensions manually retrieve latest version since we forgo 536 // doing it during list. 537 f.latestVersion, err = m.getLatestVersion(f) 538 if err != nil { 539 return err 540 } 541 return m.upgradeExtension(f, force) 542 } 543 return fmt.Errorf("no extension matched %q", name) 544 } 545 546 func (m *Manager) upgradeExtensions(exts []Extension, force bool) error { 547 var failed bool 548 for _, f := range exts { 549 fmt.Fprintf(m.io.Out, "[%s]: ", f.Name()) 550 err := m.upgradeExtension(f, force) 551 if err != nil { 552 if !errors.Is(err, localExtensionUpgradeError) && 553 !errors.Is(err, upToDateError) && 554 !errors.Is(err, pinnedExtensionUpgradeError) { 555 failed = true 556 } 557 fmt.Fprintf(m.io.Out, "%s\n", err) 558 continue 559 } 560 currentVersion := displayExtensionVersion(&f, f.currentVersion) 561 latestVersion := displayExtensionVersion(&f, f.latestVersion) 562 if m.dryRunMode { 563 fmt.Fprintf(m.io.Out, "would have upgraded from %s to %s\n", currentVersion, latestVersion) 564 } else { 565 fmt.Fprintf(m.io.Out, "upgraded from %s to %s\n", currentVersion, latestVersion) 566 } 567 } 568 if failed { 569 return errors.New("some extensions failed to upgrade") 570 } 571 return nil 572 } 573 574 func (m *Manager) upgradeExtension(ext Extension, force bool) error { 575 if ext.isLocal { 576 return localExtensionUpgradeError 577 } 578 if !force && ext.IsPinned() { 579 return pinnedExtensionUpgradeError 580 } 581 if !ext.UpdateAvailable() { 582 return upToDateError 583 } 584 var err error 585 if ext.IsBinary() { 586 err = m.upgradeBinExtension(ext) 587 } else { 588 // Check if git extension has changed to a binary extension 589 var isBin bool 590 repo, repoErr := repoFromPath(filepath.Join(ext.Path(), "..")) 591 if repoErr == nil { 592 isBin, _ = isBinExtension(m.client, repo) 593 } 594 if isBin { 595 if err := m.Remove(ext.Name()); err != nil { 596 return fmt.Errorf("failed to migrate to new precompiled extension format: %w", err) 597 } 598 return m.installBin(repo, "") 599 } 600 err = m.upgradeGitExtension(ext, force) 601 } 602 return err 603 } 604 605 func (m *Manager) upgradeGitExtension(ext Extension, force bool) error { 606 exe, err := m.lookPath("git") 607 if err != nil { 608 return err 609 } 610 dir := filepath.Dir(ext.path) 611 if m.dryRunMode { 612 return nil 613 } 614 if force { 615 if err := m.newCommand(exe, "-C", dir, "fetch", "origin", "HEAD").Run(); err != nil { 616 return err 617 } 618 return m.newCommand(exe, "-C", dir, "reset", "--hard", "origin/HEAD").Run() 619 } 620 return m.newCommand(exe, "-C", dir, "pull", "--ff-only").Run() 621 } 622 623 func (m *Manager) upgradeBinExtension(ext Extension) error { 624 repo, err := ghrepo.FromFullName(ext.url) 625 if err != nil { 626 return fmt.Errorf("failed to parse URL %s: %w", ext.url, err) 627 } 628 return m.installBin(repo, "") 629 } 630 631 func (m *Manager) Remove(name string) error { 632 targetDir := filepath.Join(m.installDir(), "gh-"+name) 633 if _, err := os.Lstat(targetDir); os.IsNotExist(err) { 634 return fmt.Errorf("no extension found: %q", targetDir) 635 } 636 if m.dryRunMode { 637 return nil 638 } 639 return os.RemoveAll(targetDir) 640 } 641 642 func (m *Manager) installDir() string { 643 return filepath.Join(m.dataDir(), "extensions") 644 } 645 646 //go:embed ext_tmpls/goBinMain.go.txt 647 var mainGoTmpl string 648 649 //go:embed ext_tmpls/goBinWorkflow.yml 650 var goBinWorkflow []byte 651 652 //go:embed ext_tmpls/otherBinWorkflow.yml 653 var otherBinWorkflow []byte 654 655 //go:embed ext_tmpls/script.sh 656 var scriptTmpl string 657 658 //go:embed ext_tmpls/buildScript.sh 659 var buildScript []byte 660 661 func (m *Manager) Create(name string, tmplType extensions.ExtTemplateType) error { 662 exe, err := m.lookPath("git") 663 if err != nil { 664 return err 665 } 666 667 if err := m.newCommand(exe, "init", "--quiet", name).Run(); err != nil { 668 return err 669 } 670 671 if tmplType == extensions.GoBinTemplateType { 672 return m.goBinScaffolding(exe, name) 673 } else if tmplType == extensions.OtherBinTemplateType { 674 return m.otherBinScaffolding(exe, name) 675 } 676 677 script := fmt.Sprintf(scriptTmpl, name) 678 if err := writeFile(filepath.Join(name, name), []byte(script), 0755); err != nil { 679 return err 680 } 681 682 return m.newCommand(exe, "-C", name, "add", name, "--chmod=+x").Run() 683 } 684 685 func (m *Manager) otherBinScaffolding(gitExe, name string) error { 686 if err := writeFile(filepath.Join(name, ".github", "workflows", "release.yml"), otherBinWorkflow, 0644); err != nil { 687 return err 688 } 689 buildScriptPath := filepath.Join("script", "build.sh") 690 if err := writeFile(filepath.Join(name, buildScriptPath), buildScript, 0755); err != nil { 691 return err 692 } 693 if err := m.newCommand(gitExe, "-C", name, "add", buildScriptPath, "--chmod=+x").Run(); err != nil { 694 return err 695 } 696 return m.newCommand(gitExe, "-C", name, "add", ".").Run() 697 } 698 699 func (m *Manager) goBinScaffolding(gitExe, name string) error { 700 goExe, err := m.lookPath("go") 701 if err != nil { 702 return fmt.Errorf("go is required for creating Go extensions: %w", err) 703 } 704 705 if err := writeFile(filepath.Join(name, ".github", "workflows", "release.yml"), goBinWorkflow, 0644); err != nil { 706 return err 707 } 708 709 mainGo := fmt.Sprintf(mainGoTmpl, name) 710 if err := writeFile(filepath.Join(name, "main.go"), []byte(mainGo), 0644); err != nil { 711 return err 712 } 713 714 host, _ := m.config.DefaultHost() 715 716 currentUser, err := api.CurrentLoginName(api.NewClientFromHTTP(m.client), host) 717 if err != nil { 718 return err 719 } 720 721 goCmds := [][]string{ 722 {"mod", "init", fmt.Sprintf("%s/%s/%s", host, currentUser, name)}, 723 {"mod", "tidy"}, 724 {"build"}, 725 } 726 727 ignore := fmt.Sprintf("/%[1]s\n/%[1]s.exe\n", name) 728 if err := writeFile(filepath.Join(name, ".gitignore"), []byte(ignore), 0644); err != nil { 729 return err 730 } 731 732 for _, args := range goCmds { 733 goCmd := m.newCommand(goExe, args...) 734 goCmd.Dir = name 735 if err := goCmd.Run(); err != nil { 736 return fmt.Errorf("failed to set up go module: %w", err) 737 } 738 } 739 740 return m.newCommand(gitExe, "-C", name, "add", ".").Run() 741 } 742 743 func isSymlink(m os.FileMode) bool { 744 return m&os.ModeSymlink != 0 745 } 746 747 func writeFile(p string, contents []byte, mode os.FileMode) error { 748 if dir := filepath.Dir(p); dir != "." { 749 if err := os.MkdirAll(dir, 0755); err != nil { 750 return err 751 } 752 } 753 return os.WriteFile(p, contents, mode) 754 } 755 756 // reads the product of makeSymlink on Windows 757 func readPathFromFile(path string) (string, error) { 758 f, err := os.Open(path) 759 if err != nil { 760 return "", err 761 } 762 defer f.Close() 763 b := make([]byte, 1024) 764 n, err := f.Read(b) 765 return strings.TrimSpace(string(b[:n])), err 766 } 767 768 func isBinExtension(client *http.Client, repo ghrepo.Interface) (isBin bool, err error) { 769 var r *release 770 r, err = fetchLatestRelease(client, repo) 771 if err != nil { 772 return 773 } 774 775 for _, a := range r.Assets { 776 dists := possibleDists() 777 for _, d := range dists { 778 suffix := d 779 if strings.HasPrefix(d, "windows") { 780 suffix += ".exe" 781 } 782 if strings.HasSuffix(a.Name, suffix) { 783 isBin = true 784 break 785 } 786 } 787 } 788 789 return 790 } 791 792 func repoFromPath(path string) (ghrepo.Interface, error) { 793 gitClient := &git.Client{RepoDir: path} 794 remotes, err := gitClient.Remotes(context.Background()) 795 if err != nil { 796 return nil, err 797 } 798 799 if len(remotes) == 0 { 800 return nil, fmt.Errorf("no remotes configured for %s", path) 801 } 802 803 var remote *git.Remote 804 805 for _, r := range remotes { 806 if r.Name == "origin" { 807 remote = r 808 break 809 } 810 } 811 812 if remote == nil { 813 remote = remotes[0] 814 } 815 816 return ghrepo.FromURL(remote.FetchURL) 817 } 818 819 func possibleDists() []string { 820 return []string{ 821 "aix-ppc64", 822 "android-386", 823 "android-amd64", 824 "android-arm", 825 "android-arm64", 826 "darwin-amd64", 827 "darwin-arm64", 828 "dragonfly-amd64", 829 "freebsd-386", 830 "freebsd-amd64", 831 "freebsd-arm", 832 "freebsd-arm64", 833 "illumos-amd64", 834 "ios-amd64", 835 "ios-arm64", 836 "js-wasm", 837 "linux-386", 838 "linux-amd64", 839 "linux-arm", 840 "linux-arm64", 841 "linux-mips", 842 "linux-mips64", 843 "linux-mips64le", 844 "linux-mipsle", 845 "linux-ppc64", 846 "linux-ppc64le", 847 "linux-riscv64", 848 "linux-s390x", 849 "netbsd-386", 850 "netbsd-amd64", 851 "netbsd-arm", 852 "netbsd-arm64", 853 "openbsd-386", 854 "openbsd-amd64", 855 "openbsd-arm", 856 "openbsd-arm64", 857 "openbsd-mips64", 858 "plan9-386", 859 "plan9-amd64", 860 "plan9-arm", 861 "solaris-amd64", 862 "windows-386", 863 "windows-amd64", 864 "windows-arm", 865 "windows-arm64", 866 } 867 } 868 869 func hasRosetta() bool { 870 _, err := os.Stat("/Library/Apple/usr/libexec/oah/libRosettaRuntime") 871 return err == nil 872 } 873 874 func codesignBinary(binPath string) error { 875 codesignExe, err := safeexec.LookPath("codesign") 876 if err != nil { 877 return err 878 } 879 cmd := exec.Command(codesignExe, "--sign", "-", "--force", "--preserve-metadata=entitlements,requirements,flags,runtime", binPath) 880 return cmd.Run() 881 }