Skip to content

Commit c1a8df0

Browse files
authored
Merge pull request #123 from cashapp/juho/git-as-package-source
Git repo as package source
2 parents c51a27b + 68d35d7 commit c1a8df0

File tree

6 files changed

+220
-52
lines changed

6 files changed

+220
-52
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ bin/hermit.hcl
1010
docs/resources/_gen
1111
docs/public
1212
.gobin/
13+
.hermit/

archive/archive.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ func Extract(b *ui.Task, source string, pkg *manifest.Package) (finalise func()
4545
// Do we need to rename the result to the final pkg.Dest?
4646
// This is set to false if we are recursively extracting packages within one another
4747
renameResult := true
48+
49+
isDir, err := isDirectory(source)
50+
if err != nil {
51+
return finalise, errors.WithStack(err)
52+
}
53+
if isDir {
54+
return finalise, installFromDirectory(source, pkg)
55+
}
56+
4857
ext := filepath.Ext(source)
4958
switch ext {
5059
case ".pkg":
@@ -145,6 +154,72 @@ type hdi struct {
145154
SystemEntities []*hdiEntry `plist:"system-entities"`
146155
}
147156

157+
func isDirectory(path string) (bool, error) {
158+
fileInfo, err := os.Stat(path)
159+
if err != nil {
160+
return false, errors.WithStack(err)
161+
}
162+
return fileInfo.IsDir(), nil
163+
}
164+
165+
func installFromDirectory(source string, pkg *manifest.Package) error {
166+
dest := pkg.Dest
167+
return errors.WithStack(filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
168+
relative, err := filepath.Rel(source, path)
169+
if err != nil {
170+
return errors.WithStack(err)
171+
}
172+
isDir, err := isDirectory(path)
173+
if err != nil {
174+
return errors.WithStack(err)
175+
}
176+
target, err := makeDestPath(dest, relative, pkg.Strip)
177+
if err != nil {
178+
return errors.WithStack(err)
179+
}
180+
if target == "" {
181+
return nil
182+
}
183+
if !isDir {
184+
if err := copyFile(path, target); err != nil {
185+
return errors.WithStack(err)
186+
}
187+
}
188+
return nil
189+
}))
190+
}
191+
192+
func copyFile(from, to string) error {
193+
err := ensureDirExists(to)
194+
if err != nil {
195+
return errors.WithStack(err)
196+
}
197+
198+
info, err := os.Stat(from)
199+
if err != nil {
200+
return errors.WithStack(err)
201+
}
202+
203+
srcFile, err := os.Open(from)
204+
if err != nil {
205+
return errors.WithStack(err)
206+
}
207+
defer srcFile.Close() // nolint: gosec
208+
209+
destFile, err := os.OpenFile(to, os.O_WRONLY|os.O_CREATE, info.Mode())
210+
if err != nil {
211+
return errors.WithStack(err)
212+
}
213+
defer destFile.Close() // nolint: gosec
214+
215+
_, err = io.Copy(destFile, srcFile)
216+
if err != nil {
217+
return errors.WithStack(err)
218+
}
219+
220+
return errors.WithStack(destFile.Sync())
221+
}
222+
148223
func installMacDMG(b *ui.Task, source string, pkg *manifest.Package) error {
149224
dest := pkg.Dest + "~"
150225
err := os.MkdirAll(dest, 0700)

archive/archive_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func TestExtract(t *testing.T) {
2929
{"linux_exe.gz", []string{"linux_exe"}},
3030
{"bzip2_1.0.6-9.2_deb10u1_amd64.deb", []string{"/bin/bzip2"}},
3131
{"bzip2-1.0.6-13.el7.x86_64.rpm", []string{"/usr/bin/bzip2"}},
32+
{"directory", []string{"foo"}},
3233
}
3334
for _, test := range tests {
3435
t.Run(test.file, func(t *testing.T) {

archive/testdata/directory/foo

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
3+
echo "bar"

cache/cache.go

Lines changed: 11 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package cache
22

33
import (
4-
"context"
54
"crypto/sha256"
65
"encoding/hex"
76
"fmt"
@@ -75,22 +74,11 @@ func (c *Cache) Create(checksum, uri string) (*os.File, error) {
7574

7675
// OpenLocal opens a local cached copy of "uri", or errors.
7776
func (c *Cache) OpenLocal(checksum, uri string) (*os.File, error) {
78-
u, err := url.Parse(uri)
77+
source, err := getSource(uri)
7978
if err != nil {
8079
return nil, errors.WithStack(err)
8180
}
82-
switch u.Scheme {
83-
case "", "file":
84-
f, err := os.Open(u.Path)
85-
return f, errors.WithStack(err)
86-
87-
case "http", "https":
88-
f, err := os.Open(c.Path(checksum, uri))
89-
return f, errors.WithStack(err)
90-
91-
default:
92-
return nil, errors.Errorf("unsupported URI: %s", uri)
93-
}
81+
return source.OpenLocal(c, checksum)
9482
}
9583

9684
// Open a local or remote artifact, transparently caching it. Subsequent accesses will use the cached copy.
@@ -119,31 +107,16 @@ func (c *Cache) Open(b *ui.Task, checksum, uri string, mirrors ...string) (*os.F
119107
//
120108
// If checksum is present it must be the SHA256 hash of the downloaded artifact.
121109
func (c *Cache) Download(b *ui.Task, checksum, uri string, mirrors ...string) (path string, etag string, err error) {
122-
cachePath := c.Path(checksum, uri)
123110
uris := append([]string{uri}, mirrors...)
124111
for _, uri := range uris {
125112
defer ui.LogElapsed(b, "Download %s", uri)()
126-
var u *url.URL
127-
u, err = url.Parse(uri)
113+
source, err := getSource(uri)
128114
if err != nil {
129115
return "", "", errors.WithStack(err)
130116
}
131-
132-
// Temporary file location.
133-
switch u.Scheme {
134-
case "", "file":
135-
// TODO: Checksum it again?
136-
// Local file, just open it.
137-
return u.Path, "", nil
138-
139-
case "http", "https":
140-
path, etag, err = c.downloadHTTP(b, checksum, uri, cachePath)
141-
142-
default:
143-
return "", "", errors.Errorf("unsupported URI %s", uri)
144-
}
117+
path, etag, err = source.Download(b, c, checksum)
145118
if err == nil {
146-
return
119+
return path, etag, nil
147120
}
148121
b.Debugf("%s: %s", uri, err)
149122
}
@@ -157,30 +130,16 @@ func (c *Cache) Download(b *ui.Task, checksum, uri string, mirrors ...string) (p
157130
// Otherwise an empty string is returned
158131
func (c *Cache) ETag(b *ui.Task, uri string, mirrors ...string) (etag string, err error) {
159132
for _, uri := range append([]string{uri}, mirrors...) {
160-
u, err := url.Parse(uri)
133+
source, err := getSource(uri)
161134
if err != nil {
162135
return "", errors.WithStack(err)
163136
}
164-
switch u.Scheme {
165-
case "http", "https":
166-
req, err := http.NewRequestWithContext(context.Background(), http.MethodHead, uri, nil)
167-
if err != nil {
168-
return "", errors.Wrap(err, uri)
169-
}
170-
resp, err := c.fastFailHTTPClient.Do(req)
171-
if err != nil {
172-
b.Debugf("%s failed: %s", uri, err)
173-
continue
174-
}
175-
defer resp.Body.Close()
176-
// Normal HTTP error, log and try the next mirror.
177-
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
178-
b.Debugf("%s failed: %d", uri, resp.StatusCode)
179-
continue
180-
}
181-
etag := resp.Header.Get("ETag")
182-
return etag, nil
137+
result, err := source.ETag(b, c)
138+
if err != nil {
139+
b.Debugf("%s failed: %s", uri, err)
140+
continue
183141
}
142+
return result, nil
184143
}
185144
return "", nil
186145
}

cache/source.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package cache
2+
3+
import (
4+
"context"
5+
"github.com/cashapp/hermit/ui"
6+
"github.com/cashapp/hermit/util"
7+
"github.com/pkg/errors"
8+
"net/http"
9+
"net/url"
10+
"os"
11+
"path/filepath"
12+
"strings"
13+
)
14+
15+
type packageSource interface {
16+
OpenLocal(cache *Cache, checksum string) (*os.File, error)
17+
Download(b *ui.Task, cache *Cache, checksum string) (path string, etag string, err error)
18+
ETag(b *ui.Task, cache *Cache) (etag string, err error)
19+
}
20+
21+
func getSource(uri string) (packageSource, error) {
22+
if strings.HasSuffix(uri, ".git") {
23+
return &gitSource{URL: uri}, nil
24+
}
25+
26+
u, err := url.Parse(uri)
27+
if err != nil {
28+
return nil, errors.WithStack(err)
29+
}
30+
31+
switch u.Scheme {
32+
case "", "file":
33+
return &fileSource{Path: u.Path}, nil
34+
35+
case "http", "https":
36+
return &httpSource{uri}, errors.WithStack(err)
37+
default:
38+
return nil, errors.Errorf("unsupported URI %s", uri)
39+
}
40+
}
41+
42+
type fileSource struct {
43+
Path string
44+
}
45+
46+
func (s *fileSource) OpenLocal(_ *Cache, _ string) (*os.File, error) {
47+
f, err := os.Open(s.Path)
48+
return f, errors.WithStack(err)
49+
}
50+
51+
func (s *fileSource) Download(_ *ui.Task, _ *Cache, _ string) (path string, etag string, err error) {
52+
// TODO: Checksum it again?
53+
// Local file, just open it.
54+
return s.Path, "", nil
55+
}
56+
57+
func (s *fileSource) ETag(_ *ui.Task, _ *Cache) (etag string, err error) {
58+
return "", nil
59+
}
60+
61+
type httpSource struct {
62+
URL string
63+
}
64+
65+
func (s *httpSource) OpenLocal(c *Cache, checksum string) (*os.File, error) {
66+
f, err := os.Open(c.Path(checksum, s.URL))
67+
return f, errors.WithStack(err)
68+
}
69+
70+
func (s *httpSource) Download(b *ui.Task, cache *Cache, checksum string) (path string, etag string, err error) {
71+
cachePath := cache.Path(checksum, s.URL)
72+
return cache.downloadHTTP(b, checksum, s.URL, cachePath)
73+
}
74+
75+
func (s *httpSource) ETag(_ *ui.Task, c *Cache) (etag string, err error) {
76+
uri := s.URL
77+
req, err := http.NewRequestWithContext(context.Background(), http.MethodHead, uri, nil)
78+
if err != nil {
79+
return "", errors.Wrap(err, uri)
80+
}
81+
resp, err := c.fastFailHTTPClient.Do(req)
82+
if err != nil {
83+
return "", errors.Wrap(err, uri)
84+
}
85+
defer resp.Body.Close()
86+
// Normal HTTP error, log and try the next mirror.
87+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
88+
return "", errors.Errorf("%s failed: %d", uri, resp.StatusCode)
89+
}
90+
result := resp.Header.Get("ETag")
91+
return result, nil
92+
}
93+
94+
type gitSource struct {
95+
URL string
96+
}
97+
98+
func (s *gitSource) OpenLocal(c *Cache, checksum string) (*os.File, error) {
99+
f, err := os.Open(c.Path(checksum, s.URL))
100+
return f, errors.WithStack(err)
101+
}
102+
103+
func (s *gitSource) Download(b *ui.Task, cache *Cache, checksum string) (string, string, error) {
104+
base := BasePath(checksum, s.URL)
105+
err := util.RunInDir(b, cache.root, "git", "clone", "--depth=1", s.URL, base)
106+
if err != nil {
107+
return "", "", errors.Wrap(err, s.URL)
108+
}
109+
etag, err := s.ETag(b, cache)
110+
if err != nil {
111+
return "", "", errors.Wrap(err, s.URL)
112+
}
113+
114+
return filepath.Join(cache.root, base), etag, nil
115+
}
116+
117+
func (s *gitSource) ETag(b *ui.Task, c *Cache) (etag string, err error) {
118+
bts, err := util.Capture(b, "git", "ls-remote", s.URL, "HEAD")
119+
if err != nil {
120+
return "", errors.Wrap(err, s.URL)
121+
}
122+
str := string(bts)
123+
parts := strings.Split(str, "\t")
124+
if len(parts) != 2 {
125+
return "", errors.Errorf("invalid HEAD: %s", str)
126+
}
127+
128+
return parts[0], nil
129+
}

0 commit comments

Comments
 (0)