Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use hard links on Windows #3005

Merged
merged 19 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/windows-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,9 @@ jobs:
- run: go install ./cmd/aqua
if: inputs.aqua_version == ''

- run: |
$(if($env:AQUA_ROOT_DIR) {echo $env:AQUA_ROOT_DIR} else {echo "$HOME/AppData/Local/aquaproj-aqua/bin"}) | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- run: aqua policy allow
- run: echo "$HOME\AppData\Local\aquaproj-aqua\bat" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- run: echo "AQUA_GLOBAL_CONFIG=$PWD\tests\main\aqua-global.yaml;$PWD\tests\main\aqua-global-2.yaml" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- run: echo "standard,kubernetes-sigs/kind" | aqua g -f -
- run: echo "x-motemen/ghq" | aqua g -f -
Expand Down
15 changes: 7 additions & 8 deletions pkg/controller/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import (
// This method is also called by "cp" command.
func (c *Controller) Install(ctx context.Context, logE *logrus.Entry, param *config.Param) error {
if param.Dest == "" {
// Create "bin" and "bat" directories and install aqua-proxy in advance.
// Create a "bin" directory and install aqua-proxy in advance.
// If param.Dest isn't empty, this means this method is called by "copy" command.
// If the command is "copy", this block is skipped.
if err := c.mkBinBatDir(); err != nil {
if err := c.mkBinDir(); err != nil {
return err
}
if err := c.packageInstaller.InstallProxy(ctx, logE); err != nil {
Expand Down Expand Up @@ -53,14 +53,13 @@ func (c *Controller) Install(ctx context.Context, logE *logrus.Entry, param *con
return c.installAll(ctx, logE, param, policyCfgs, globalPolicyPaths)
}

func (c *Controller) mkBinBatDir() error {
rootBin := filepath.Join(c.rootDir, "bin")
if err := osfile.MkdirAll(c.fs, rootBin); err != nil {
func (c *Controller) mkBinDir() error {
if err := osfile.MkdirAll(c.fs, filepath.Join(c.rootDir, "bin")); err != nil {
return fmt.Errorf("create the directory: %w", err)
}
if c.runtime.GOOS == "windows" {
if err := osfile.MkdirAll(c.fs, filepath.Join(c.rootDir, "bat")); err != nil {
return fmt.Errorf("create the directory: %w", err)
if c.runtime.IsWindows() {
if err := c.fs.RemoveAll(filepath.Join(c.rootDir, "bat")); err != nil {
return fmt.Errorf("remove the bat directory: %w", err)
}
}
return nil
Expand Down
3 changes: 1 addition & 2 deletions pkg/controller/which/lookpath_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ func (c *Controller) listExts() []string {

func (c *Controller) lookPath(envPath, exeName string) string {
binDir := filepath.Join(c.rootDir, "bin")
batDir := filepath.Join(c.rootDir, "bat")
exts := c.listExts()
for _, p := range filepath.SplitList(envPath) {
if p == binDir || p == batDir {
if p == binDir {
continue
}
bin := filepath.Join(p, exeName)
Expand Down
6 changes: 5 additions & 1 deletion pkg/installpackage/aqua.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ func (is *Installer) InstallAqua(ctx context.Context, logE *logrus.Entry, versio
func (is *Installer) copyAquaOnWindows(exePath string) error {
// https://github.com/orgs/aquaproj/discussions/2510
// https://stackoverflow.com/questions/1211948/best-method-for-implementing-self-updating-software
// https://github.com/aquaproj/aqua/issues/2918
dest := filepath.Join(is.rootDir, "bin", "aqua.exe")
if f, err := afero.Exists(is.fs, dest); err != nil {
return fmt.Errorf("check if aqua.exe exists: %w", err)
Expand All @@ -164,5 +165,8 @@ func (is *Installer) copyAquaOnWindows(exePath string) error {
return fmt.Errorf("rename aqua.exe to update: %w", err)
}
}
return is.Copy(dest, exePath)
if err := is.linker.Hardlink(exePath, dest); err != nil {
return fmt.Errorf("create a hard link to aqua.exe: %w", err)
}
return nil
}
1 change: 1 addition & 0 deletions pkg/installpackage/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ func newInstaller(param *config.Param, downloader download.ClientAPI, rt *runtim
type Linker interface {
Lstat(s string) (os.FileInfo, error)
Symlink(dest, src string) error
Hardlink(dest, src string) error
Readlink(src string) (string, error)
}

Expand Down
120 changes: 62 additions & 58 deletions pkg/installpackage/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/aquaproj/aqua/v2/pkg/config"
Expand All @@ -14,12 +15,39 @@ import (

func (is *Installer) createLinks(logE *logrus.Entry, pkgs []*config.Package) bool {
failed := false

var aquaProxyPathOnWindows string
if is.runtime.IsWindows() {
pkg := proxyPkg()
pkgPath, err := pkg.PkgPath(is.rootDir, is.runtime)
if err != nil {
logerr.WithError(logE, err).Error("get a path to aqua-proxy")
failed = true
}
aquaProxyPathOnWindows = filepath.Join(pkgPath, "aqua-proxy.exe")
}

for _, pkg := range pkgs {
pkgInfo := pkg.PackageInfo
for _, file := range pkgInfo.GetFiles() {
if isWindows(is.runtime.GOOS) {
if err := is.createProxyWindows(file.Name, logE); err != nil {
logerr.WithError(logE, err).Error("create the proxy file")
if isWindows(runtime.GOOS) {
hardLink := filepath.Join(is.rootDir, "bin", file.Name+".exe")
if f, err := afero.Exists(is.fs, hardLink); err != nil {
logerr.WithError(logE, err).WithFields(logrus.Fields{
"command": file.Name,
}).Error("check if a hard link to aqua-proxy exists")
failed = true
continue
} else if f {
continue
}
logE.WithFields(logrus.Fields{
"command": file.Name,
}).Info("creating a hard link to aqua-proxy")
if err := is.linker.Hardlink(aquaProxyPathOnWindows, hardLink); err != nil {
logerr.WithError(logE, err).WithFields(logrus.Fields{
"command": file.Name,
}).Error("create a hard link to aqua-proxy")
failed = true
}
continue
Expand All @@ -34,6 +62,37 @@ func (is *Installer) createLinks(logE *logrus.Entry, pkgs []*config.Package) boo
return failed
}

func (is *Installer) recreateHardLinks() error {
binDir := filepath.Join(is.rootDir, "bin")
infos, err := afero.ReadDir(is.fs, binDir)
if err != nil {
return fmt.Errorf("read a bin dir: %w", err)
}

pkg := proxyPkg()
pkgPath, err := pkg.PkgPath(is.rootDir, is.runtime)
if err != nil {
return err //nolint:wrapcheck
}
a := filepath.Join(pkgPath, "aqua-proxy.exe")

for _, info := range infos {
if info.Name() == "aqua.exe" {
continue
}
p := filepath.Join(binDir, info.Name())
if err := is.fs.Remove(p); err != nil {
return fmt.Errorf("remove a file to replace it with a hard link: %w", err)
}
if strings.HasSuffix(info.Name(), ".exe") {
if err := is.linker.Hardlink(a, p); err != nil {
return fmt.Errorf("create a hard link: %w", err)
}
}
}
return nil
}

func (is *Installer) createLink(linkPath, linkDest string, logE *logrus.Entry) error {
if fileInfo, err := is.linker.Lstat(linkPath); err == nil {
switch mode := fileInfo.Mode(); {
Expand Down Expand Up @@ -90,58 +149,3 @@ func (is *Installer) recreateLink(linkPath, linkDest string, logE *logrus.Entry)
}
return nil
}

const (
batTemplate = `@echo off
aqua exec -- <COMMAND> %*
`
scrTemplate = `#!/usr/bin/env bash
exec aqua exec -- "$0" "$@"
`
proxyPermission os.FileMode = 0o755
)

func (is *Installer) createProxyWindows(binName string, logE *logrus.Entry) error {
if err := is.createBinWindows(filepath.Join(is.rootDir, "bin", binName), scrTemplate, logE); err != nil {
return err
}
if err := is.createBinWindows(filepath.Join(is.rootDir, "bat", binName+".bat"), strings.Replace(batTemplate, "<COMMAND>", binName, 1), logE); err != nil {
return err
}
return nil
}

func (is *Installer) createBinWindows(binPath, binTxt string, logE *logrus.Entry) error {
if fileInfo, err := is.linker.Lstat(binPath); err == nil {
switch mode := fileInfo.Mode(); {
case mode.IsDir():
// if file is a directory, raise error
return fmt.Errorf("%s has already existed and is a directory", binPath)
case mode&os.ModeNamedPipe != 0:
// if file is a pipe, raise error
return fmt.Errorf("%s has already existed and is a named pipe", binPath)
case mode.IsRegular():
// TODO check content
return nil
case mode&os.ModeSymlink != 0:
if err := is.fs.Remove(binPath); err != nil {
return fmt.Errorf("remove a symbolic link (%s): %w", binPath, err)
}
return is.writeBinWindows(binPath, binTxt, logE)
default:
return fmt.Errorf("unexpected file mode %s: %s", binPath, mode.String())
}
}

return is.writeBinWindows(binPath, binTxt, logE)
}

func (is *Installer) writeBinWindows(proxyPath, binTxt string, logE *logrus.Entry) error {
logE.WithFields(logrus.Fields{
"proxy_path": proxyPath,
}).Info("create a proxy file")
if err := afero.WriteFile(is.fs, proxyPath, []byte(binTxt), proxyPermission); err != nil {
return fmt.Errorf("create a proxy file (%s): %w", proxyPath, err)
}
return nil
}
17 changes: 17 additions & 0 deletions pkg/installpackage/linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ func (lk *mockLinker) Symlink(dest, src string) error {
return nil
}

func (lk *mockLinker) Hardlink(dest, src string) error {
if _, ok := lk.files[src]; ok {
return errors.New("file already exists")
}
if _, err := lk.fs.Create(src); err != nil {
return err //nolint:wrapcheck
}
if lk.files == nil {
lk.files = map[string]*mockFileInfo{}
}
lk.files[src] = &mockFileInfo{
Dest: dest,
name: filepath.Base(src),
}
return nil
}

func (lk *mockLinker) Readlink(src string) (string, error) {
if f, ok := lk.files[src]; ok {
return f.Dest, nil
Expand Down
44 changes: 27 additions & 17 deletions pkg/installpackage/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"path/filepath"
"runtime"

"github.com/aquaproj/aqua/v2/pkg/checksum"
"github.com/aquaproj/aqua/v2/pkg/config"
Expand All @@ -12,24 +13,21 @@ import (
"github.com/sirupsen/logrus"
)

const ProxyVersion = "v1.2.6" // renovate: depName=aquaproj/aqua-proxy
const ProxyVersion = "v1.2.7" // renovate: depName=aquaproj/aqua-proxy

func ProxyChecksums() map[string]string {
return map[string]string{
"darwin/amd64": "b7bc0d22040bf450fbb31a8a92acabee11e33735e59a0692574ee57fbe4695be",
"darwin/arm64": "2f0a551c14cae1823d2cde2530348032ce04aff5bfe9b5d8fd5815a366d7a9e5",
"linux/amd64": "8fa0876fb9c6b23b9f911fa49ec8edc750badec86eb30b47e8aff231d82d12dd",
"linux/arm64": "4694cbbdf7e2a22778baf75c424db59c3810d795792234b72be1166d432786ef",
"windows/amd64": "86ec4a91f4bee061482070ac1b6e519a37a27313372af259e367799f413d8dfd",
"windows/arm64": "5e81237a2117b5f3f7a41c84181f722bfcb1032b7af8ff3977a94e1995dd0c47",
"darwin/amd64": "1EA8D0CD0A1BDCEE463B04DB7DF9DBF50AB54549B9EB9FE6506A15CDFA8F4563",
"darwin/arm64": "3686358F141D39A2909F9A6467E37B48DEF9767F7AF872483D43B4C9A5C5DF93",
"linux/amd64": "6917C867B818FA0B261E28C1924EFB33820D8FFF3930093D16B4E76793773F81",
"linux/arm64": "5E7B8A403E4B20251B02D0AEE537249C107DD6743DBD1F22A40EE6A077AC7DE9",
"windows/amd64": "9C953DABD95A6231CF3C5C1A20781007AB44E603160C0865836DE57F3307976A",
"windows/arm64": "4CBC6E4002EB5322A74E2B331B5BDAC188D4B678EF93DD2D2BED1AEF71683A8E",
}
}

func (is *Installer) InstallProxy(ctx context.Context, logE *logrus.Entry) error { //nolint:funlen
if isWindows(is.runtime.GOOS) {
return nil
}
pkg := &config.Package{
func proxyPkg() *config.Package {
return &config.Package{
Package: &aqua.Package{
Name: proxyName,
Version: ProxyVersion,
Expand All @@ -46,6 +44,10 @@ func (is *Installer) InstallProxy(ctx context.Context, logE *logrus.Entry) error
},
},
}
}

func (is *Installer) InstallProxy(ctx context.Context, logE *logrus.Entry) error {
pkg := proxyPkg()
logE = logE.WithFields(logrus.Fields{
"package_name": pkg.Package.Name,
"package_version": pkg.Package.Version,
Expand All @@ -62,6 +64,14 @@ func (is *Installer) InstallProxy(ctx context.Context, logE *logrus.Entry) error
if err != nil {
return err //nolint:wrapcheck
}

// create a symbolic link
binName := proxyName
a, err := filepath.Rel(is.rootDir, filepath.Join(pkgPath, binName))
if err != nil {
return fmt.Errorf("get a relative path: %w", err)
}

logE.Debug("check if aqua-proxy is already installed")
finfo, err := is.fs.Stat(pkgPath)
if err != nil {
Expand All @@ -78,17 +88,17 @@ func (is *Installer) InstallProxy(ctx context.Context, logE *logrus.Entry) error
}); err != nil {
return err
}
if isWindows(runtime.GOOS) {
return is.recreateHardLinks()
}
} else { //nolint:gocritic
if !finfo.IsDir() {
return fmt.Errorf("%s isn't a directory", pkgPath)
}
}

// create a symbolic link
binName := proxyName
a, err := filepath.Rel(is.rootDir, filepath.Join(pkgPath, binName))
if err != nil {
return fmt.Errorf("get a relative path: %w", err)
if isWindows(runtime.GOOS) {
return nil
}

return is.createLink(filepath.Join(is.rootDir, proxyName), a, logE)
Expand Down
4 changes: 4 additions & 0 deletions pkg/link/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ func (lk *Linker) Symlink(dest, src string) error {
return os.Symlink(dest, src) //nolint:wrapcheck
}

func (lk *Linker) Hardlink(dest, src string) error {
return os.Link(dest, src) //nolint:wrapcheck
}

func (lk *Linker) Readlink(src string) (string, error) {
return os.Readlink(src) //nolint:wrapcheck
}
4 changes: 4 additions & 0 deletions pkg/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ func NewR() *Runtime {
}
}

func (rt *Runtime) IsWindows() bool {
return rt.GOOS == "windows"
}

func (rt *Runtime) Env() string {
return fmt.Sprintf("%s/%s", rt.GOOS, rt.GOARCH)
}
Expand Down