Skip to content

Commit e21dc7f

Browse files
committed
snapshot
1 parent ab50c39 commit e21dc7f

File tree

4 files changed

+298
-0
lines changed

4 files changed

+298
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Build toolbox
2+
3+
on:
4+
push:
5+
branches: [ master, main ]
6+
paths:
7+
- 'toolbox/**'
8+
- 'app/playbooks/**'
9+
- '.github/workflows/build-toolbox.yml'
10+
workflow_dispatch: {}
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
build:
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
include:
21+
- runner: ubuntu-24.04
22+
arch_tag: x86_64-linux
23+
- runner: ubuntu-24.04-arm64
24+
arch_tag: aarch64-linux
25+
runs-on: ${{ matrix.runner }}
26+
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@v4
30+
31+
- name: Setup Go
32+
uses: actions/setup-go@v5
33+
with:
34+
go-version: '1.22.x'
35+
36+
- name: Install Nix
37+
uses: cachix/install-nix-action@v27
38+
39+
- name: Build toolbox builder
40+
shell: bash
41+
run: |
42+
set -euo pipefail
43+
cd toolbox
44+
go build -o toolbox-builder
45+
46+
- name: Build toolbox archive
47+
shell: bash
48+
run: |
49+
set -euo pipefail
50+
cd toolbox
51+
./toolbox-builder -yaml ../app/playbooks/60-second-linux.yaml -out toolbox-${{ matrix.arch_tag }}.tar.xz
52+
53+
- name: Upload artifact
54+
uses: actions/upload-artifact@v4
55+
with:
56+
name: toolbox-${{ matrix.arch_tag }}
57+
path: toolbox/toolbox-${{ matrix.arch_tag }}.tar.xz
58+
if-no-files-found: error
59+
retention-days: 7
60+
61+

toolbox/go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module gradient-toolbox
2+
3+
go 1.22
4+
5+
require (
6+
gopkg.in/yaml.v3 v3.0.1
7+
github.com/ulikunitz/xz v0.5.14
8+
)
9+
10+

toolbox/go.sum

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
2+
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
3+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
4+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
5+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

toolbox/main.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"flag"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"runtime"
13+
"time"
14+
15+
"gopkg.in/yaml.v3"
16+
)
17+
18+
type playbookConfig struct {
19+
Nixpkgs struct {
20+
Version string `yaml:"version"`
21+
Packages []string `yaml:"packages"`
22+
} `yaml:"nixpkgs"`
23+
}
24+
25+
func main() {
26+
var yamlPath string
27+
var outPath string
28+
var workDir string
29+
30+
flag.StringVar(&yamlPath, "yaml", "", "Path to YAML with nixpkgs.packages")
31+
flag.StringVar(&outPath, "out", "toolbox.tar.xz", "Output archive path")
32+
flag.StringVar(&workDir, "workdir", ".", "Working directory where toolbox/ is created")
33+
flag.Parse()
34+
35+
if yamlPath == "" {
36+
fmt.Fprintln(os.Stderr, "error: -yaml path is required")
37+
os.Exit(2)
38+
}
39+
if runtime.GOOS != "linux" {
40+
fmt.Fprintln(os.Stderr, "error: this utility must run on Linux")
41+
os.Exit(2)
42+
}
43+
44+
cfg, err := readYAML(yamlPath)
45+
if err != nil {
46+
fatalf("failed to read YAML: %v", err)
47+
}
48+
if len(cfg.Nixpkgs.Packages) == 0 {
49+
fatalf("no nixpkgs.packages listed in %s", yamlPath)
50+
}
51+
52+
toolboxDir, _ := filepath.Abs(filepath.Join(workDir, "toolbox"))
53+
_ = exec.Command("chmod", "-R", "u+w", toolboxDir).Run()
54+
_ = exec.Command("rm", "-rf", toolboxDir).Run()
55+
56+
defer func() {
57+
_ = exec.Command("chmod", "-R", "u+w", toolboxDir).Run()
58+
_ = exec.Command("rm", "-rf", toolboxDir).Run()
59+
}()
60+
61+
if err := nixCopy(toolboxDir, cfg.Nixpkgs.Packages); err != nil {
62+
fatalf("nix copy failed: %v", err)
63+
}
64+
65+
if err := fetchAndInstallProot(toolboxDir); err != nil {
66+
fatalf("failed to install proot: %v", err)
67+
}
68+
69+
if err := createTarXz(outPath, toolboxDir); err != nil {
70+
fatalf("failed to create tar.xz: %v", err)
71+
}
72+
73+
fmt.Printf("created %s\n", outPath)
74+
}
75+
76+
func readYAML(path string) (*playbookConfig, error) {
77+
data, err := os.ReadFile(path)
78+
if err != nil {
79+
return nil, err
80+
}
81+
var cfg playbookConfig
82+
if err := yaml.Unmarshal(data, &cfg); err != nil {
83+
return nil, err
84+
}
85+
return &cfg, nil
86+
}
87+
88+
func nixCopy(destDir string, pkgs []string) error {
89+
if _, err := exec.LookPath("nix"); err != nil {
90+
return fmt.Errorf("nix not found in PATH: %w", err)
91+
}
92+
args := []string{
93+
"--extra-experimental-features", "flakes",
94+
"--extra-experimental-features", "nix-command",
95+
"copy",
96+
"--to", destDir,
97+
}
98+
for _, p := range pkgs {
99+
args = append(args, "nixpkgs#"+p)
100+
}
101+
cmd := exec.Command("nix", args...)
102+
cmd.Stdout = os.Stdout
103+
cmd.Stderr = os.Stderr
104+
return cmd.Run()
105+
}
106+
107+
func fetchAndInstallProot(destDir string) error {
108+
arch := runtime.GOARCH
109+
var url string
110+
// Values provided by user; nix base32 style digests
111+
switch arch {
112+
case "amd64":
113+
url = "https://web.archive.org/web/20240412082958if_/http://dl-cdn.alpinelinux.org/alpine/edge/testing/x86_64/proot-static-5.4.0-r0.apk"
114+
case "arm64":
115+
url = "https://web.archive.org/web/20240412083320if_/http://dl-cdn.alpinelinux.org/alpine/edge/testing/aarch64/proot-static-5.4.0-r0.apk"
116+
default:
117+
return fmt.Errorf("unsupported architecture %s; only amd64 and arm64 are supported", arch)
118+
}
119+
120+
client := &http.Client{Timeout: 60 * time.Second}
121+
resp, err := client.Get(url)
122+
if err != nil {
123+
return err
124+
}
125+
defer resp.Body.Close()
126+
if resp.StatusCode != http.StatusOK {
127+
return fmt.Errorf("download failed: %s", resp.Status)
128+
}
129+
buf := &bytes.Buffer{}
130+
if _, err := io.Copy(buf, resp.Body); err != nil {
131+
return err
132+
}
133+
134+
if err := extractProotFromAPK(buf.Bytes(), filepath.Join(destDir, "proot")); err != nil {
135+
return err
136+
}
137+
return nil
138+
}
139+
140+
141+
142+
func extractProotFromAPK(apk []byte, destPath string) error {
143+
workDir, err := os.MkdirTemp("", "proot_apk_*")
144+
if err != nil {
145+
return err
146+
}
147+
defer os.RemoveAll(workDir)
148+
149+
apkPath := filepath.Join(workDir, "proot.apk")
150+
if err := os.WriteFile(apkPath, apk, 0o644); err != nil {
151+
return err
152+
}
153+
154+
gzPath := apkPath + ".tar.gz"
155+
if err := os.Rename(apkPath, gzPath); err != nil {
156+
return err
157+
}
158+
159+
cmd := exec.Command("tar", "-xzf", gzPath, "-C", workDir)
160+
cmd.Stdout = os.Stdout
161+
cmd.Stderr = os.Stderr
162+
if err := cmd.Run(); err != nil {
163+
return fmt.Errorf("tar extract failed: %w", err)
164+
}
165+
166+
candidate := filepath.Join(workDir, "usr", "bin", "proot.static")
167+
if _, err := os.Stat(candidate); err == nil {
168+
return copyExecutable(candidate, destPath)
169+
}
170+
171+
var found string
172+
_ = filepath.WalkDir(workDir, func(p string, d os.DirEntry, _ error) error {
173+
if d != nil && !d.IsDir() && filepath.Base(p) == "proot.static" {
174+
found = p
175+
return io.EOF
176+
}
177+
return nil
178+
})
179+
if found == "" {
180+
return fmt.Errorf("proot.static not found in APK")
181+
}
182+
return copyExecutable(found, destPath)
183+
}
184+
185+
func copyExecutable(src, dst string) error {
186+
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
187+
return err
188+
}
189+
srcF, err := os.Open(src)
190+
if err != nil {
191+
return err
192+
}
193+
defer srcF.Close()
194+
195+
dstF, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
196+
if err != nil {
197+
return err
198+
}
199+
if _, err := io.Copy(dstF, srcF); err != nil {
200+
dstF.Close()
201+
return err
202+
}
203+
return dstF.Close()
204+
}
205+
206+
func createTarXz(outPath string, dir string) error {
207+
parent := filepath.Dir(dir)
208+
base := filepath.Base(dir)
209+
210+
cmd := exec.Command("tar", "-I", "xz -e -9 -T0", "-cf", outPath, base)
211+
cmd.Dir = parent
212+
cmd.Stdout = os.Stdout
213+
cmd.Stderr = os.Stderr
214+
return cmd.Run()
215+
}
216+
217+
func fatalf(format string, a ...any) {
218+
fmt.Fprintf(os.Stderr, format+"\n", a...)
219+
os.Exit(1)
220+
}
221+
222+

0 commit comments

Comments
 (0)