Skip to content

Commit 6aa6dd7

Browse files
feat: use hard links on Windows (#3005)
* feat: use hard links on Windows * fix: remove bat directory * fix: fix lint errors * fix: add the file extension .exe on Windows * fix: fix hardlink args * refactor: simplify codes * fix: install aqua-proxy before replacing scripts with hard links * fix: fix hard links * fix: remove .exe * chore: update aqua-proxy to v1.2.7-1 * ci: fix test on windows * fix: remove files without .exe on bin * fix: create a hard link instead of copy aqua * fix: use a hard link when updating aqua * fix: skip recreating aqua.exe when aqua-proxy is installed * fix: fix a lint error * fix: use runtime instead of fake runtime * chore: update aqua-proxy to 1.2.7
1 parent 59c5897 commit 6aa6dd7

File tree

10 files changed

+130
-87
lines changed

10 files changed

+130
-87
lines changed

.github/workflows/windows-test.yaml

+2-1
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,9 @@ jobs:
136136
- run: go install ./cmd/aqua
137137
if: inputs.aqua_version == ''
138138

139+
- run: |
140+
$(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
139141
- run: aqua policy allow
140-
- run: echo "$HOME\AppData\Local\aquaproj-aqua\bat" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
141142
- 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
142143
- run: echo "standard,kubernetes-sigs/kind" | aqua g -f -
143144
- run: echo "x-motemen/ghq" | aqua g -f -

pkg/controller/install/install.go

+7-8
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import (
1919
// This method is also called by "cp" command.
2020
func (c *Controller) Install(ctx context.Context, logE *logrus.Entry, param *config.Param) error {
2121
if param.Dest == "" {
22-
// Create "bin" and "bat" directories and install aqua-proxy in advance.
22+
// Create a "bin" directory and install aqua-proxy in advance.
2323
// If param.Dest isn't empty, this means this method is called by "copy" command.
2424
// If the command is "copy", this block is skipped.
25-
if err := c.mkBinBatDir(); err != nil {
25+
if err := c.mkBinDir(); err != nil {
2626
return err
2727
}
2828
if err := c.packageInstaller.InstallProxy(ctx, logE); err != nil {
@@ -53,14 +53,13 @@ func (c *Controller) Install(ctx context.Context, logE *logrus.Entry, param *con
5353
return c.installAll(ctx, logE, param, policyCfgs, globalPolicyPaths)
5454
}
5555

56-
func (c *Controller) mkBinBatDir() error {
57-
rootBin := filepath.Join(c.rootDir, "bin")
58-
if err := osfile.MkdirAll(c.fs, rootBin); err != nil {
56+
func (c *Controller) mkBinDir() error {
57+
if err := osfile.MkdirAll(c.fs, filepath.Join(c.rootDir, "bin")); err != nil {
5958
return fmt.Errorf("create the directory: %w", err)
6059
}
61-
if c.runtime.GOOS == "windows" {
62-
if err := osfile.MkdirAll(c.fs, filepath.Join(c.rootDir, "bat")); err != nil {
63-
return fmt.Errorf("create the directory: %w", err)
60+
if c.runtime.IsWindows() {
61+
if err := c.fs.RemoveAll(filepath.Join(c.rootDir, "bat")); err != nil {
62+
return fmt.Errorf("remove the bat directory: %w", err)
6463
}
6564
}
6665
return nil

pkg/controller/which/lookpath_windows.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,9 @@ func (c *Controller) listExts() []string {
2929

3030
func (c *Controller) lookPath(envPath, exeName string) string {
3131
binDir := filepath.Join(c.rootDir, "bin")
32-
batDir := filepath.Join(c.rootDir, "bat")
3332
exts := c.listExts()
3433
for _, p := range filepath.SplitList(envPath) {
35-
if p == binDir || p == batDir {
34+
if p == binDir {
3635
continue
3736
}
3837
bin := filepath.Join(p, exeName)

pkg/installpackage/aqua.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ func (is *Installer) InstallAqua(ctx context.Context, logE *logrus.Entry, versio
150150
func (is *Installer) copyAquaOnWindows(exePath string) error {
151151
// https://github.com/orgs/aquaproj/discussions/2510
152152
// https://stackoverflow.com/questions/1211948/best-method-for-implementing-self-updating-software
153+
// https://github.com/aquaproj/aqua/issues/2918
153154
dest := filepath.Join(is.rootDir, "bin", "aqua.exe")
154155
if f, err := afero.Exists(is.fs, dest); err != nil {
155156
return fmt.Errorf("check if aqua.exe exists: %w", err)
@@ -164,5 +165,8 @@ func (is *Installer) copyAquaOnWindows(exePath string) error {
164165
return fmt.Errorf("rename aqua.exe to update: %w", err)
165166
}
166167
}
167-
return is.Copy(dest, exePath)
168+
if err := is.linker.Hardlink(exePath, dest); err != nil {
169+
return fmt.Errorf("create a hard link to aqua.exe: %w", err)
170+
}
171+
return nil
168172
}

pkg/installpackage/installer.go

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ func newInstaller(param *config.Param, downloader download.ClientAPI, rt *runtim
9090
type Linker interface {
9191
Lstat(s string) (os.FileInfo, error)
9292
Symlink(dest, src string) error
93+
Hardlink(dest, src string) error
9394
Readlink(src string) (string, error)
9495
}
9596

pkg/installpackage/link.go

+62-58
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"runtime"
78
"strings"
89

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

1516
func (is *Installer) createLinks(logE *logrus.Entry, pkgs []*config.Package) bool {
1617
failed := false
18+
19+
var aquaProxyPathOnWindows string
20+
if is.runtime.IsWindows() {
21+
pkg := proxyPkg()
22+
pkgPath, err := pkg.PkgPath(is.rootDir, is.runtime)
23+
if err != nil {
24+
logerr.WithError(logE, err).Error("get a path to aqua-proxy")
25+
failed = true
26+
}
27+
aquaProxyPathOnWindows = filepath.Join(pkgPath, "aqua-proxy.exe")
28+
}
29+
1730
for _, pkg := range pkgs {
1831
pkgInfo := pkg.PackageInfo
1932
for _, file := range pkgInfo.GetFiles() {
20-
if isWindows(is.runtime.GOOS) {
21-
if err := is.createProxyWindows(file.Name, logE); err != nil {
22-
logerr.WithError(logE, err).Error("create the proxy file")
33+
if isWindows(runtime.GOOS) {
34+
hardLink := filepath.Join(is.rootDir, "bin", file.Name+".exe")
35+
if f, err := afero.Exists(is.fs, hardLink); err != nil {
36+
logerr.WithError(logE, err).WithFields(logrus.Fields{
37+
"command": file.Name,
38+
}).Error("check if a hard link to aqua-proxy exists")
39+
failed = true
40+
continue
41+
} else if f {
42+
continue
43+
}
44+
logE.WithFields(logrus.Fields{
45+
"command": file.Name,
46+
}).Info("creating a hard link to aqua-proxy")
47+
if err := is.linker.Hardlink(aquaProxyPathOnWindows, hardLink); err != nil {
48+
logerr.WithError(logE, err).WithFields(logrus.Fields{
49+
"command": file.Name,
50+
}).Error("create a hard link to aqua-proxy")
2351
failed = true
2452
}
2553
continue
@@ -34,6 +62,37 @@ func (is *Installer) createLinks(logE *logrus.Entry, pkgs []*config.Package) boo
3462
return failed
3563
}
3664

65+
func (is *Installer) recreateHardLinks() error {
66+
binDir := filepath.Join(is.rootDir, "bin")
67+
infos, err := afero.ReadDir(is.fs, binDir)
68+
if err != nil {
69+
return fmt.Errorf("read a bin dir: %w", err)
70+
}
71+
72+
pkg := proxyPkg()
73+
pkgPath, err := pkg.PkgPath(is.rootDir, is.runtime)
74+
if err != nil {
75+
return err //nolint:wrapcheck
76+
}
77+
a := filepath.Join(pkgPath, "aqua-proxy.exe")
78+
79+
for _, info := range infos {
80+
if info.Name() == "aqua.exe" {
81+
continue
82+
}
83+
p := filepath.Join(binDir, info.Name())
84+
if err := is.fs.Remove(p); err != nil {
85+
return fmt.Errorf("remove a file to replace it with a hard link: %w", err)
86+
}
87+
if strings.HasSuffix(info.Name(), ".exe") {
88+
if err := is.linker.Hardlink(a, p); err != nil {
89+
return fmt.Errorf("create a hard link: %w", err)
90+
}
91+
}
92+
}
93+
return nil
94+
}
95+
3796
func (is *Installer) createLink(linkPath, linkDest string, logE *logrus.Entry) error {
3897
if fileInfo, err := is.linker.Lstat(linkPath); err == nil {
3998
switch mode := fileInfo.Mode(); {
@@ -90,58 +149,3 @@ func (is *Installer) recreateLink(linkPath, linkDest string, logE *logrus.Entry)
90149
}
91150
return nil
92151
}
93-
94-
const (
95-
batTemplate = `@echo off
96-
aqua exec -- <COMMAND> %*
97-
`
98-
scrTemplate = `#!/usr/bin/env bash
99-
exec aqua exec -- "$0" "$@"
100-
`
101-
proxyPermission os.FileMode = 0o755
102-
)
103-
104-
func (is *Installer) createProxyWindows(binName string, logE *logrus.Entry) error {
105-
if err := is.createBinWindows(filepath.Join(is.rootDir, "bin", binName), scrTemplate, logE); err != nil {
106-
return err
107-
}
108-
if err := is.createBinWindows(filepath.Join(is.rootDir, "bat", binName+".bat"), strings.Replace(batTemplate, "<COMMAND>", binName, 1), logE); err != nil {
109-
return err
110-
}
111-
return nil
112-
}
113-
114-
func (is *Installer) createBinWindows(binPath, binTxt string, logE *logrus.Entry) error {
115-
if fileInfo, err := is.linker.Lstat(binPath); err == nil {
116-
switch mode := fileInfo.Mode(); {
117-
case mode.IsDir():
118-
// if file is a directory, raise error
119-
return fmt.Errorf("%s has already existed and is a directory", binPath)
120-
case mode&os.ModeNamedPipe != 0:
121-
// if file is a pipe, raise error
122-
return fmt.Errorf("%s has already existed and is a named pipe", binPath)
123-
case mode.IsRegular():
124-
// TODO check content
125-
return nil
126-
case mode&os.ModeSymlink != 0:
127-
if err := is.fs.Remove(binPath); err != nil {
128-
return fmt.Errorf("remove a symbolic link (%s): %w", binPath, err)
129-
}
130-
return is.writeBinWindows(binPath, binTxt, logE)
131-
default:
132-
return fmt.Errorf("unexpected file mode %s: %s", binPath, mode.String())
133-
}
134-
}
135-
136-
return is.writeBinWindows(binPath, binTxt, logE)
137-
}
138-
139-
func (is *Installer) writeBinWindows(proxyPath, binTxt string, logE *logrus.Entry) error {
140-
logE.WithFields(logrus.Fields{
141-
"proxy_path": proxyPath,
142-
}).Info("create a proxy file")
143-
if err := afero.WriteFile(is.fs, proxyPath, []byte(binTxt), proxyPermission); err != nil {
144-
return fmt.Errorf("create a proxy file (%s): %w", proxyPath, err)
145-
}
146-
return nil
147-
}

pkg/installpackage/linker.go

+17
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,23 @@ func (lk *mockLinker) Symlink(dest, src string) error {
4444
return nil
4545
}
4646

47+
func (lk *mockLinker) Hardlink(dest, src string) error {
48+
if _, ok := lk.files[src]; ok {
49+
return errors.New("file already exists")
50+
}
51+
if _, err := lk.fs.Create(src); err != nil {
52+
return err //nolint:wrapcheck
53+
}
54+
if lk.files == nil {
55+
lk.files = map[string]*mockFileInfo{}
56+
}
57+
lk.files[src] = &mockFileInfo{
58+
Dest: dest,
59+
name: filepath.Base(src),
60+
}
61+
return nil
62+
}
63+
4764
func (lk *mockLinker) Readlink(src string) (string, error) {
4865
if f, ok := lk.files[src]; ok {
4966
return f.Dest, nil

pkg/installpackage/proxy.go

+27-17
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"path/filepath"
7+
"runtime"
78

89
"github.com/aquaproj/aqua/v2/pkg/checksum"
910
"github.com/aquaproj/aqua/v2/pkg/config"
@@ -12,24 +13,21 @@ import (
1213
"github.com/sirupsen/logrus"
1314
)
1415

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

1718
func ProxyChecksums() map[string]string {
1819
return map[string]string{
19-
"darwin/amd64": "b7bc0d22040bf450fbb31a8a92acabee11e33735e59a0692574ee57fbe4695be",
20-
"darwin/arm64": "2f0a551c14cae1823d2cde2530348032ce04aff5bfe9b5d8fd5815a366d7a9e5",
21-
"linux/amd64": "8fa0876fb9c6b23b9f911fa49ec8edc750badec86eb30b47e8aff231d82d12dd",
22-
"linux/arm64": "4694cbbdf7e2a22778baf75c424db59c3810d795792234b72be1166d432786ef",
23-
"windows/amd64": "86ec4a91f4bee061482070ac1b6e519a37a27313372af259e367799f413d8dfd",
24-
"windows/arm64": "5e81237a2117b5f3f7a41c84181f722bfcb1032b7af8ff3977a94e1995dd0c47",
20+
"darwin/amd64": "1EA8D0CD0A1BDCEE463B04DB7DF9DBF50AB54549B9EB9FE6506A15CDFA8F4563",
21+
"darwin/arm64": "3686358F141D39A2909F9A6467E37B48DEF9767F7AF872483D43B4C9A5C5DF93",
22+
"linux/amd64": "6917C867B818FA0B261E28C1924EFB33820D8FFF3930093D16B4E76793773F81",
23+
"linux/arm64": "5E7B8A403E4B20251B02D0AEE537249C107DD6743DBD1F22A40EE6A077AC7DE9",
24+
"windows/amd64": "9C953DABD95A6231CF3C5C1A20781007AB44E603160C0865836DE57F3307976A",
25+
"windows/arm64": "4CBC6E4002EB5322A74E2B331B5BDAC188D4B678EF93DD2D2BED1AEF71683A8E",
2526
}
2627
}
2728

28-
func (is *Installer) InstallProxy(ctx context.Context, logE *logrus.Entry) error { //nolint:funlen
29-
if isWindows(is.runtime.GOOS) {
30-
return nil
31-
}
32-
pkg := &config.Package{
29+
func proxyPkg() *config.Package {
30+
return &config.Package{
3331
Package: &aqua.Package{
3432
Name: proxyName,
3533
Version: ProxyVersion,
@@ -46,6 +44,10 @@ func (is *Installer) InstallProxy(ctx context.Context, logE *logrus.Entry) error
4644
},
4745
},
4846
}
47+
}
48+
49+
func (is *Installer) InstallProxy(ctx context.Context, logE *logrus.Entry) error {
50+
pkg := proxyPkg()
4951
logE = logE.WithFields(logrus.Fields{
5052
"package_name": pkg.Package.Name,
5153
"package_version": pkg.Package.Version,
@@ -62,6 +64,14 @@ func (is *Installer) InstallProxy(ctx context.Context, logE *logrus.Entry) error
6264
if err != nil {
6365
return err //nolint:wrapcheck
6466
}
67+
68+
// create a symbolic link
69+
binName := proxyName
70+
a, err := filepath.Rel(is.rootDir, filepath.Join(pkgPath, binName))
71+
if err != nil {
72+
return fmt.Errorf("get a relative path: %w", err)
73+
}
74+
6575
logE.Debug("check if aqua-proxy is already installed")
6676
finfo, err := is.fs.Stat(pkgPath)
6777
if err != nil {
@@ -78,17 +88,17 @@ func (is *Installer) InstallProxy(ctx context.Context, logE *logrus.Entry) error
7888
}); err != nil {
7989
return err
8090
}
91+
if isWindows(runtime.GOOS) {
92+
return is.recreateHardLinks()
93+
}
8194
} else { //nolint:gocritic
8295
if !finfo.IsDir() {
8396
return fmt.Errorf("%s isn't a directory", pkgPath)
8497
}
8598
}
8699

87-
// create a symbolic link
88-
binName := proxyName
89-
a, err := filepath.Rel(is.rootDir, filepath.Join(pkgPath, binName))
90-
if err != nil {
91-
return fmt.Errorf("get a relative path: %w", err)
100+
if isWindows(runtime.GOOS) {
101+
return nil
92102
}
93103

94104
return is.createLink(filepath.Join(is.rootDir, proxyName), a, logE)

pkg/link/link.go

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ func (lk *Linker) Symlink(dest, src string) error {
1818
return os.Symlink(dest, src) //nolint:wrapcheck
1919
}
2020

21+
func (lk *Linker) Hardlink(dest, src string) error {
22+
return os.Link(dest, src) //nolint:wrapcheck
23+
}
24+
2125
func (lk *Linker) Readlink(src string) (string, error) {
2226
return os.Readlink(src) //nolint:wrapcheck
2327
}

pkg/runtime/runtime.go

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ func NewR() *Runtime {
2525
}
2626
}
2727

28+
func (rt *Runtime) IsWindows() bool {
29+
return rt.GOOS == "windows"
30+
}
31+
2832
func (rt *Runtime) Env() string {
2933
return fmt.Sprintf("%s/%s", rt.GOOS, rt.GOARCH)
3034
}

0 commit comments

Comments
 (0)