Skip to content

Commit 10b4dce

Browse files
committed
unpack: keep cache
The cache is now kept in `${XDG_RUNTIME_DIR}/gomodjail/<HASH_OF_PATH>`. Needed for nerdctl. Signed-off-by: Akihiro Suda <[email protected]>
1 parent ac0db7a commit 10b4dce

File tree

4 files changed

+127
-35
lines changed

4 files changed

+127
-35
lines changed

cmd/gomodjail/main.go

Lines changed: 21 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/AkihiroSuda/gomodjail/cmd/gomodjail/commands/pack"
1414
"github.com/AkihiroSuda/gomodjail/cmd/gomodjail/commands/run"
1515
"github.com/AkihiroSuda/gomodjail/cmd/gomodjail/version"
16+
"github.com/AkihiroSuda/gomodjail/pkg/cache"
1617
"github.com/AkihiroSuda/gomodjail/pkg/env"
1718
"github.com/AkihiroSuda/gomodjail/pkg/envutil"
1819
"github.com/AkihiroSuda/gomodjail/pkg/osargs"
@@ -24,19 +25,12 @@ import (
2425
var logLevel = new(slog.LevelVar)
2526

2627
func main() {
27-
exitCode, closer := xmain()
28-
if closer != nil {
29-
if cErr := closer(); cErr != nil {
30-
slog.Error("failed to call closer", "error", cErr)
31-
}
32-
}
33-
if exitCode != 0 {
28+
if exitCode := xmain(); exitCode != 0 {
3429
os.Exit(exitCode)
3530
}
3631
}
3732

38-
func xmain() (int, func() error) {
39-
var closer func() error
33+
func xmain() int {
4034
logHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel})
4135
slog.SetDefault(slog.New(logHandler))
4236
rootCmd := newRootCommand()
@@ -47,13 +41,13 @@ func xmain() (int, func() error) {
4741
}
4842
if zr != nil {
4943
var err error
50-
err, closer = configureSelfExtractMode(rootCmd, zr)
44+
err = configureSelfExtractMode(rootCmd, zr)
5145
if cErr := zr.Close(); cErr != nil {
5246
slog.Error("failed to call closer", "error", cErr)
5347
}
5448
if err != nil {
5549
slog.Error("exiting with an error while setting up self-extract mode", "error", err)
56-
return 1, closer
50+
return 1
5751
}
5852
}
5953
}
@@ -72,9 +66,9 @@ func xmain() (int, func() error) {
7266
} else {
7367
slog.Debug("exiting")
7468
}
75-
return exitCode, closer
69+
return exitCode
7670
}
77-
return 0, closer
71+
return 0
7872
}
7973

8074
func newRootCommand() *cobra.Command {
@@ -108,58 +102,54 @@ func newRootCommand() *cobra.Command {
108102
return cmd
109103
}
110104

111-
//nolint:staticcheck // ST1008: error should be returned as the last argument
112-
func configureSelfExtractMode(rootCmd *cobra.Command, zr *zip.ReadCloser) (error, func() error) {
105+
func configureSelfExtractMode(rootCmd *cobra.Command, zr *zip.ReadCloser) error {
113106
slog.Debug("Running in self-extract mode")
114107

115-
td, err := os.MkdirTemp("", "gomodjail-*")
108+
// td must be kept on exit
109+
// https://github.com/containerd/nerdctl/pull/4012#issuecomment-2840539282
110+
td, err := cache.ExecutableDir()
116111
if err != nil {
117-
return err, nil
112+
return err
118113
}
119-
slog.Debug("created self-extract dir", "path", td)
120-
closer := func() error {
121-
slog.Debug("removing self-extract dir", "path", td)
122-
return os.RemoveAll(td)
123-
}
124-
114+
slog.Debug("unpacking self-extract archive", "dir", td)
125115
fis, err := ziputil.Unzip(td, zr)
126116
if err != nil {
127-
return fmt.Errorf("failed to unzip to %q: %w", td, err), closer
117+
return fmt.Errorf("failed to unzip to %q: %w", td, err)
128118
}
129119
var libgomodjailHookFI, progFI, goModFI fs.FileInfo
130120
switch runtime.GOOS {
131121
case "darwin":
132122
if len(fis) != 3 {
133-
return fmt.Errorf("expected an archive to contain 3 files (libgomodjail_hook_darwin.dylib, program and go.mod), got %d files", len(fis)), closer
123+
return fmt.Errorf("expected an archive to contain 3 files (libgomodjail_hook_darwin.dylib, program and go.mod), got %d files", len(fis))
134124
}
135125
libgomodjailHookFI, progFI, goModFI = fis[0], fis[1], fis[2]
136126
default:
137127
if len(fis) != 2 {
138-
return fmt.Errorf("expected an archive to contain 2 files (program and go.mod), got %d files", len(fis)), closer
128+
return fmt.Errorf("expected an archive to contain 2 files (program and go.mod), got %d files", len(fis))
139129
}
140130
progFI, goModFI = fis[0], fis[1]
141131
}
142132
if filepath.Base(progFI.Name()) != progFI.Name() {
143-
return fmt.Errorf("unexpected file name: %q", progFI.Name()), closer
133+
return fmt.Errorf("unexpected file name: %q", progFI.Name())
144134
}
145135
if goModFI.Name() != "go.mod" {
146-
return fmt.Errorf("expected \"go.mod\", got %q", goModFI.Name()), closer
136+
return fmt.Errorf("expected \"go.mod\", got %q", goModFI.Name())
147137
}
148138
prog := filepath.Join(td, progFI.Name())
149139
goMod := filepath.Join(td, goModFI.Name())
150140
switch runtime.GOOS {
151141
case "darwin":
152142
if libgomodjailHookFI.Name() != "libgomodjail_hook_darwin.dylib" {
153-
return fmt.Errorf("expected \"libgomodjail_hook_darwin.dylib\", got %q", libgomodjailHookFI.Name()), closer
143+
return fmt.Errorf("expected \"libgomodjail_hook_darwin.dylib\", got %q", libgomodjailHookFI.Name())
154144
}
155145
libgomodjailHook := filepath.Join(td, libgomodjailHookFI.Name())
156146
if err = os.Setenv("LIBGOMODJAIL_HOOK", libgomodjailHook); err != nil {
157-
return err, closer
147+
return err
158148
}
159149
}
160150
args := append([]string{os.Args[0], "run", "--go-mod=" + goMod, prog, "--"}, os.Args[1:]...)
161151
slog.Debug("Reconfiguring the top-level command", "args", args)
162152
rootCmd.SetArgs(args[1:])
163153
osargs.SetOSArgs(args)
164-
return nil, closer
154+
return nil
165155
}

pkg/cache/cache.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package cache
2+
3+
import (
4+
"crypto/sha256"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"runtime"
9+
"strconv"
10+
11+
"github.com/AkihiroSuda/gomodjail/pkg/env"
12+
)
13+
14+
func ExecutableDir() (string, error) {
15+
selfPath, err := os.Executable()
16+
if err != nil {
17+
return "", err
18+
}
19+
20+
cacheHome, err := Home()
21+
if err != nil {
22+
return "", fmt.Errorf("failed to resolve GOMODJAIL_CACHE_HOME: %w", err)
23+
}
24+
25+
selfPathDigest := sha256sum([]byte(selfPath))
26+
selfPathDigestPartial := selfPathDigest[0:16]
27+
28+
dir := filepath.Join(cacheHome, selfPathDigestPartial)
29+
return dir, nil
30+
}
31+
32+
func sha256sum(b []byte) string {
33+
h := sha256.New()
34+
if _, err := h.Write(b); err != nil {
35+
panic(err)
36+
}
37+
return fmt.Sprintf("%x", h.Sum(nil))
38+
}
39+
40+
// Home candidates are:
41+
// - $GOMODJAIL_CACHE_HOME
42+
// - $XDG_RUNTIME_DIR/gomodjail
43+
// - $TMPDIR/gomodjail (macOS)
44+
// - $XDG_CACHE_HOME/gomodjail
45+
func Home() (string, error) {
46+
if cacheHome := os.Getenv(env.CacheHome); cacheHome != "" {
47+
return cacheHome, nil
48+
}
49+
if xrd := xdgRuntimeDir(); xrd != "" {
50+
cacheHome := filepath.Join(xrd, "gomodjail")
51+
return cacheHome, nil
52+
}
53+
if runtime.GOOS == "darwin" {
54+
// macOS allocates a unique TMPDIR per user
55+
if td := os.Getenv("TMPDIR"); td != "" {
56+
cacheHome := filepath.Join(td, "gomodjail")
57+
return cacheHome, nil
58+
}
59+
}
60+
osCacheHome, err := os.UserCacheDir()
61+
if err != nil {
62+
return "", err
63+
}
64+
cacheHome := filepath.Join(osCacheHome, "gomodjail")
65+
return cacheHome, nil
66+
}
67+
68+
func xdgRuntimeDir() string {
69+
if xrd := os.Getenv("XDG_RUNTIME_DIR"); xrd != "" {
70+
return xrd
71+
}
72+
xrd := filepath.Join("run", "user", strconv.Itoa(os.Geteuid()))
73+
if _, err := os.Stat(xrd); err == nil {
74+
return xrd
75+
}
76+
return ""
77+
}

pkg/env/env.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
package env
22

3-
const PrivateChild = "_GOMODJAIL_PRIVATE_CHILD" // no value
3+
const (
4+
PrivateChild = "_GOMODJAIL_PRIVATE_CHILD" // no value
5+
CacheHome = "GOMODJAIL_CACHE_HOME"
6+
)

pkg/ziputil/ziputil.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"io/fs"
9+
"log/slog"
910
"math"
1011
"os"
1112
"path/filepath"
@@ -70,6 +71,13 @@ func FindSelfExtractArchive() (*zip.ReadCloser, error) {
7071
}
7172

7273
func Unzip(dir string, zr *zip.ReadCloser) ([]fs.FileInfo, error) {
74+
// https://specifications.freedesktop.org/basedir-spec/latest/#variables
75+
// To ensure that your files are not removed, they should have their access time timestamp modified at least once every 6 hours of monotonic time
76+
// or the 'sticky' bit should be set on the file.
77+
if err := os.MkdirAll(dir, 0o755|os.ModeSticky); err != nil {
78+
return nil, err
79+
}
80+
7381
res := make([]fs.FileInfo, len(zr.File))
7482
for i, f := range zr.File {
7583
if err := unzip1(dir, f); err != nil {
@@ -96,7 +104,16 @@ func unzip1(dir string, f *zip.File) error {
96104
return fmt.Errorf("unexpected file: %q", fs.FormatFileInfo(fi))
97105
}
98106
wPath := filepath.Join(dir, baseName)
99-
w, err := os.OpenFile(wPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode())
107+
modTime := fi.ModTime()
108+
if st, err := os.Stat(wPath); err == nil && st.ModTime().Equal(modTime) && modTime.UnixNano() != 0 {
109+
// TODO: compare digest too (via xattr? fs-verity?)
110+
slog.Debug("already exists", "path", wPath, "modTime", modTime)
111+
return nil
112+
}
113+
114+
wPathTmp := fmt.Sprintf("%s.pid-%d", wPath, os.Getpid()) // for atomicity
115+
defer os.RemoveAll(wPathTmp)
116+
w, err := os.OpenFile(wPathTmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode()|os.ModeSticky)
100117
if err != nil {
101118
return err
102119
}
@@ -109,8 +126,13 @@ func unzip1(dir string, f *zip.File) error {
109126
return err
110127
}
111128
}
112-
modTime := fi.ModTime()
113-
if err = os.Chtimes(wPath, modTime, modTime); err != nil {
129+
if err = os.Chtimes(wPathTmp, modTime, modTime); err != nil {
130+
return err
131+
}
132+
if err = os.RemoveAll(wPath); err != nil {
133+
return err
134+
}
135+
if err = os.Rename(wPathTmp, wPath); err != nil {
114136
return err
115137
}
116138
return nil

0 commit comments

Comments
 (0)