Skip to content

Commit 57cdb89

Browse files
authored
Merge pull request #48 from AkihiroSuda/dev
unpack: keep cache
2 parents ac0db7a + 3c9d3b1 commit 57cdb89

File tree

4 files changed

+129
-36
lines changed

4 files changed

+129
-36
lines changed

cmd/gomodjail/main.go

Lines changed: 21 additions & 32 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()
@@ -46,14 +40,13 @@ func xmain() (int, func() error) {
4640
slog.Error("error while detecting self-extract archive", "error", err)
4741
}
4842
if zr != nil {
49-
var err error
50-
err, closer = configureSelfExtractMode(rootCmd, zr)
43+
err := configureSelfExtractMode(rootCmd, zr)
5144
if cErr := zr.Close(); cErr != nil {
5245
slog.Error("failed to call closer", "error", cErr)
5346
}
5447
if err != nil {
5548
slog.Error("exiting with an error while setting up self-extract mode", "error", err)
56-
return 1, closer
49+
return 1
5750
}
5851
}
5952
}
@@ -72,9 +65,9 @@ func xmain() (int, func() error) {
7265
} else {
7366
slog.Debug("exiting")
7467
}
75-
return exitCode, closer
68+
return exitCode
7669
}
77-
return 0, closer
70+
return 0
7871
}
7972

8073
func newRootCommand() *cobra.Command {
@@ -108,58 +101,54 @@ func newRootCommand() *cobra.Command {
108101
return cmd
109102
}
110103

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

115-
td, err := os.MkdirTemp("", "gomodjail-*")
107+
// td must be kept on exit
108+
// https://github.com/containerd/nerdctl/pull/4012#issuecomment-2840539282
109+
td, err := cache.ExecutableDir()
116110
if err != nil {
117-
return err, nil
111+
return err
118112
}
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-
113+
slog.Debug("unpacking self-extract archive", "dir", td)
125114
fis, err := ziputil.Unzip(td, zr)
126115
if err != nil {
127-
return fmt.Errorf("failed to unzip to %q: %w", td, err), closer
116+
return fmt.Errorf("failed to unzip to %q: %w", td, err)
128117
}
129118
var libgomodjailHookFI, progFI, goModFI fs.FileInfo
130119
switch runtime.GOOS {
131120
case "darwin":
132121
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
122+
return fmt.Errorf("expected an archive to contain 3 files (libgomodjail_hook_darwin.dylib, program and go.mod), got %d files", len(fis))
134123
}
135124
libgomodjailHookFI, progFI, goModFI = fis[0], fis[1], fis[2]
136125
default:
137126
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
127+
return fmt.Errorf("expected an archive to contain 2 files (program and go.mod), got %d files", len(fis))
139128
}
140129
progFI, goModFI = fis[0], fis[1]
141130
}
142131
if filepath.Base(progFI.Name()) != progFI.Name() {
143-
return fmt.Errorf("unexpected file name: %q", progFI.Name()), closer
132+
return fmt.Errorf("unexpected file name: %q", progFI.Name())
144133
}
145134
if goModFI.Name() != "go.mod" {
146-
return fmt.Errorf("expected \"go.mod\", got %q", goModFI.Name()), closer
135+
return fmt.Errorf("expected \"go.mod\", got %q", goModFI.Name())
147136
}
148137
prog := filepath.Join(td, progFI.Name())
149138
goMod := filepath.Join(td, goModFI.Name())
150139
switch runtime.GOOS {
151140
case "darwin":
152141
if libgomodjailHookFI.Name() != "libgomodjail_hook_darwin.dylib" {
153-
return fmt.Errorf("expected \"libgomodjail_hook_darwin.dylib\", got %q", libgomodjailHookFI.Name()), closer
142+
return fmt.Errorf("expected \"libgomodjail_hook_darwin.dylib\", got %q", libgomodjailHookFI.Name())
154143
}
155144
libgomodjailHook := filepath.Join(td, libgomodjailHookFI.Name())
156145
if err = os.Setenv("LIBGOMODJAIL_HOOK", libgomodjailHook); err != nil {
157-
return err, closer
146+
return err
158147
}
159148
}
160149
args := append([]string{os.Args[0], "run", "--go-mod=" + goMod, prog, "--"}, os.Args[1:]...)
161150
slog.Debug("Reconfiguring the top-level command", "args", args)
162151
rootCmd.SetArgs(args[1:])
163152
osargs.SetOSArgs(args)
164-
return nil, closer
153+
return nil
165154
}

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: 27 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,18 @@ 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+
// for atomicity
115+
wPathTmp := fmt.Sprintf("%s.pid-%d", wPath, os.Getpid())
116+
defer os.RemoveAll(wPathTmp) //nolint:errcheck
117+
118+
w, err := os.OpenFile(wPathTmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode()|os.ModeSticky)
100119
if err != nil {
101120
return err
102121
}
@@ -109,8 +128,13 @@ func unzip1(dir string, f *zip.File) error {
109128
return err
110129
}
111130
}
112-
modTime := fi.ModTime()
113-
if err = os.Chtimes(wPath, modTime, modTime); err != nil {
131+
if err = os.Chtimes(wPathTmp, modTime, modTime); err != nil {
132+
return err
133+
}
134+
if err = os.RemoveAll(wPath); err != nil {
135+
return err
136+
}
137+
if err = os.Rename(wPathTmp, wPath); err != nil {
114138
return err
115139
}
116140
return nil

0 commit comments

Comments
 (0)