From 14c868bd327c22bc3c9f68c4a21d9155462235ed Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 13 Nov 2022 21:21:51 +1100 Subject: [PATCH] feat: Git manifest sources can now specify a reference to pin to This was in the documentation but not implemented (as discovered in #342). `#` was supported for Git packages, so I've factored that code out and reused it. Added an integration test for this, along with a couple of new test helper functions that should make these kind of tests simpler in the future. --- cache/git.go | 29 +--- docs/content/packaging/schema/channel.md | 3 +- docs/content/packaging/schema/darwin.md | 3 +- docs/content/packaging/schema/linux.md | 3 +- docs/content/packaging/schema/manifest.md | 3 +- docs/content/packaging/schema/platform.md | 3 +- docs/content/packaging/schema/version.md | 3 +- docs/content/usage/config.md | 2 +- integration/integration_test.go | 178 ++++++++++++++++------ integration/testdata/git-source.zip | Bin 0 -> 18708 bytes manifest/config.go | 2 +- sources/git.go | 4 +- sources/git_test.go | 7 + sources/sources.go | 5 +- util/git.go | 44 ++++++ cache/source_test.go => util/git_test.go | 6 +- util/run.go | 10 ++ 17 files changed, 222 insertions(+), 83 deletions(-) create mode 100644 integration/testdata/git-source.zip create mode 100644 util/git.go rename cache/source_test.go => util/git_test.go (68%) diff --git a/cache/git.go b/cache/git.go index 70d9be16b..46b243c1a 100644 --- a/cache/git.go +++ b/cache/git.go @@ -23,27 +23,15 @@ func (s *gitSource) OpenLocal(c *Cache, checksum string) (*os.File, error) { func (s *gitSource) Download(b *ui.Task, cache *Cache, checksum string) (string, string, string, error) { base := BasePath(checksum, s.URL) checkoutDir := filepath.Join(cache.root, base) - repo, tag := parseGitURL(s.URL) - args := []string{"git", "clone", "--depth=1", repo, checkoutDir} - if tag != "" { - args = append(args, "--branch="+tag) - } - err := util.RunInDir(b, cache.root, args...) - if err != nil { - return "", "", "", errors.WithStack(err) - } - - bts, err := util.CaptureInDir(b, checkoutDir, "git", "rev-parse", "HEAD") + etag, err := util.GitClone(b, &util.RealCommandRunner{}, s.URL, checkoutDir) if err != nil { - return "", "", "", errors.WithStack(err) + return "", "", "", errors.Wrap(err, s.URL) } - etag := strings.Trim(string(bts), "\n") - return filepath.Join(cache.root, base), etag, "", nil } func (s *gitSource) ETag(b *ui.Task) (etag string, err error) { - repo, tag := parseGitURL(s.URL) + repo, tag := util.ParseGitURL(s.URL) if tag == "" { tag = "HEAD" } @@ -61,7 +49,7 @@ func (s *gitSource) ETag(b *ui.Task) (etag string, err error) { } func (s *gitSource) Validate() error { - repo, tag := parseGitURL(s.URL) + repo, tag := util.ParseGitURL(s.URL) if tag == "" { tag = "HEAD" } @@ -72,12 +60,3 @@ func (s *gitSource) Validate() error { } return nil } - -func parseGitURL(source string) (repo, tag string) { - parts := strings.SplitN(source, "#", 2) - repo = parts[0] - if len(parts) > 1 { - tag = parts[1] - } - return -} diff --git a/docs/content/packaging/schema/channel.md b/docs/content/packaging/schema/channel.md index 34917c3b7..5da2cc1ca 100644 --- a/docs/content/packaging/schema/channel.md +++ b/docs/content/packaging/schema/channel.md @@ -36,7 +36,8 @@ Used by: [<manifest>](../manifest#blocks) | `root` | `string?` | Override root for package. | | `runtime-dependencies` | `[string]?` | Packages used internally by this package, but not installed to the target environment | | `sha256` | `string?` | SHA256 of source package for verification. When in conflict with SHA256 in sha256sums, this value takes precedence. | -| `source` | `string?` | URL for source package. Valid URLs are Git repositories (using .git[#<tag>] suffix), Local Files (using file:// prefix), and Remote Files (using http:// or https:// prefix) | +| `sha256-source` | `string?` | URL for SHA256 checksum file for source package. | +| `source` | `string?` | URL for source package. Valid URLs are Git repositories (using .git[#<ref>] suffix), Local Files (using file:// prefix), and Remote Files (using http:// or https:// prefix) | | `strip` | `number?` | Number of path prefix elements to strip. | | `test` | `string?` | Command that will test the package is operational. | | `update` | `string` | Update frequency for this channel. | diff --git a/docs/content/packaging/schema/darwin.md b/docs/content/packaging/schema/darwin.md index 81cb10a21..1a7957662 100644 --- a/docs/content/packaging/schema/darwin.md +++ b/docs/content/packaging/schema/darwin.md @@ -36,7 +36,8 @@ Used by: [channel](../channel#blocks) [linux](../linux#blocks) [<manifest>](. | `root` | `string?` | Override root for package. | | `runtime-dependencies` | `[string]?` | Packages used internally by this package, but not installed to the target environment | | `sha256` | `string?` | SHA256 of source package for verification. When in conflict with SHA256 in sha256sums, this value takes precedence. | -| `source` | `string?` | URL for source package. Valid URLs are Git repositories (using .git[#<tag>] suffix), Local Files (using file:// prefix), and Remote Files (using http:// or https:// prefix) | +| `sha256-source` | `string?` | URL for SHA256 checksum file for source package. | +| `source` | `string?` | URL for source package. Valid URLs are Git repositories (using .git[#<ref>] suffix), Local Files (using file:// prefix), and Remote Files (using http:// or https:// prefix) | | `strip` | `number?` | Number of path prefix elements to strip. | | `test` | `string?` | Command that will test the package is operational. | | `vars` | `{string: string}?` | Set local variables used during manifest evaluation. | diff --git a/docs/content/packaging/schema/linux.md b/docs/content/packaging/schema/linux.md index bbf54326b..b0f8c2e84 100644 --- a/docs/content/packaging/schema/linux.md +++ b/docs/content/packaging/schema/linux.md @@ -36,7 +36,8 @@ Used by: [channel](../channel#blocks) [darwin](../darwin#blocks) [<manifest>] | `root` | `string?` | Override root for package. | | `runtime-dependencies` | `[string]?` | Packages used internally by this package, but not installed to the target environment | | `sha256` | `string?` | SHA256 of source package for verification. When in conflict with SHA256 in sha256sums, this value takes precedence. | -| `source` | `string?` | URL for source package. Valid URLs are Git repositories (using .git[#<tag>] suffix), Local Files (using file:// prefix), and Remote Files (using http:// or https:// prefix) | +| `sha256-source` | `string?` | URL for SHA256 checksum file for source package. | +| `source` | `string?` | URL for source package. Valid URLs are Git repositories (using .git[#<ref>] suffix), Local Files (using file:// prefix), and Remote Files (using http:// or https:// prefix) | | `strip` | `number?` | Number of path prefix elements to strip. | | `test` | `string?` | Command that will test the package is operational. | | `vars` | `{string: string}?` | Set local variables used during manifest evaluation. | diff --git a/docs/content/packaging/schema/manifest.md b/docs/content/packaging/schema/manifest.md index af8ed0a7d..acd105080 100644 --- a/docs/content/packaging/schema/manifest.md +++ b/docs/content/packaging/schema/manifest.md @@ -40,8 +40,9 @@ Each Hermit package manifest is a nested structure containing OS/architecture-sp | `root` | `string?` | Override root for package. | | `runtime-dependencies` | `[string]?` | Packages used internally by this package, but not installed to the target environment | | `sha256` | `string?` | SHA256 of source package for verification. When in conflict with SHA256 in sha256sums, this value takes precedence. | +| `sha256-source` | `string?` | URL for SHA256 checksum file for source package. | | `sha256sums` | `{string: string}?` | SHA256 checksums of source packages for verification. | -| `source` | `string?` | URL for source package. Valid URLs are Git repositories (using .git[#<tag>] suffix), Local Files (using file:// prefix), and Remote Files (using http:// or https:// prefix) | +| `source` | `string?` | URL for source package. Valid URLs are Git repositories (using .git[#<ref>] suffix), Local Files (using file:// prefix), and Remote Files (using http:// or https:// prefix) | | `strip` | `number?` | Number of path prefix elements to strip. | | `test` | `string?` | Command that will test the package is operational. | | `vars` | `{string: string}?` | Set local variables used during manifest evaluation. | diff --git a/docs/content/packaging/schema/platform.md b/docs/content/packaging/schema/platform.md index 311a6272d..cd84fe2cf 100644 --- a/docs/content/packaging/schema/platform.md +++ b/docs/content/packaging/schema/platform.md @@ -36,7 +36,8 @@ Used by: [channel](../channel#blocks) [darwin](../darwin#blocks) [linux](../linu | `root` | `string?` | Override root for package. | | `runtime-dependencies` | `[string]?` | Packages used internally by this package, but not installed to the target environment | | `sha256` | `string?` | SHA256 of source package for verification. When in conflict with SHA256 in sha256sums, this value takes precedence. | -| `source` | `string?` | URL for source package. Valid URLs are Git repositories (using .git[#<tag>] suffix), Local Files (using file:// prefix), and Remote Files (using http:// or https:// prefix) | +| `sha256-source` | `string?` | URL for SHA256 checksum file for source package. | +| `source` | `string?` | URL for source package. Valid URLs are Git repositories (using .git[#<ref>] suffix), Local Files (using file:// prefix), and Remote Files (using http:// or https:// prefix) | | `strip` | `number?` | Number of path prefix elements to strip. | | `test` | `string?` | Command that will test the package is operational. | | `vars` | `{string: string}?` | Set local variables used during manifest evaluation. | diff --git a/docs/content/packaging/schema/version.md b/docs/content/packaging/schema/version.md index 65d8f9db8..a208894a4 100644 --- a/docs/content/packaging/schema/version.md +++ b/docs/content/packaging/schema/version.md @@ -37,7 +37,8 @@ Used by: [<manifest>](../manifest#blocks) | `root` | `string?` | Override root for package. | | `runtime-dependencies` | `[string]?` | Packages used internally by this package, but not installed to the target environment | | `sha256` | `string?` | SHA256 of source package for verification. When in conflict with SHA256 in sha256sums, this value takes precedence. | -| `source` | `string?` | URL for source package. Valid URLs are Git repositories (using .git[#<tag>] suffix), Local Files (using file:// prefix), and Remote Files (using http:// or https:// prefix) | +| `sha256-source` | `string?` | URL for SHA256 checksum file for source package. | +| `source` | `string?` | URL for source package. Valid URLs are Git repositories (using .git[#<ref>] suffix), Local Files (using file:// prefix), and Remote Files (using http:// or https:// prefix) | | `strip` | `number?` | Number of path prefix elements to strip. | | `test` | `string?` | Command that will test the package is operational. | | `vars` | `{string: string}?` | Set local variables used during manifest evaluation. | diff --git a/docs/content/usage/config.md b/docs/content/usage/config.md index a075facd2..3ee7b7bec 100644 --- a/docs/content/usage/config.md +++ b/docs/content/usage/config.md @@ -18,7 +18,7 @@ customise that Hermit environment. Hermit supports three different manifest sources: -1. Git repositories; any cloneable URI ending with `.git`, eg.
`https://github.com/cashapp/hermit-packages.git`. An optional `#` suffix can be added to checkout a specific tag. +1. Git repositories; any cloneable URI ending with `.git`, eg.
`https://github.com/cashapp/hermit-packages.git`. An optional `#` suffix can be added to checkout a specific reference. 2. Local filesystem, eg. `file:///home/user/my-packages`.
This is mostly only useful for local development and testing. 3. Environment relative, eg. `env:///my-packages`.
This will search for package manifests in the directory `${HERMIT_ENV}/my-packages`. Useful for local overrides. diff --git a/integration/integration_test.go b/integration/integration_test.go index 743155955..c102a1406 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -1,4 +1,4 @@ -// Package integration provides integration tests for Hermit. +// Package integration_test provides integration tests for Hermit. // // Each test is run against the supported shells, in a temporary directory, with // a version of Hermit built from the current source. @@ -13,11 +13,13 @@ package integration_test import ( + "archive/zip" "bufio" "fmt" "io" "io/fs" "io/ioutil" + "net/http" "os" "os/exec" "path/filepath" @@ -31,48 +33,6 @@ import ( "github.com/creack/pty" ) -var shells = [][]string{ - {"bash", "--norc", "--noprofile"}, - {"zsh", "--no-rcs", "--no-globalrcs"}, -} - -// Functions that test scripts can use to communicate back to the test framework. -const preamble = ` -set -euo pipefail - -hermit-send() { - echo "$@" 1>&3 -} - -assert() { - if ! "$@"; then - hermit-send "error: assertion failed: $@" - exit 1 - fi -} - -# Run a shell command and emulate what the Hermit shell hooks would do. -# -# usage: with-prompt-hooks -# -# Normally this is done by shell hooks, but because we're not running interactively this is not possible. -with-prompt-hooks() { - "$@" - res=$? - # We need to reset the change timestamp, as file timestamps are at second resolution. - # Some IT updates could be lost without this - export HERMIT_BIN_CHANGE=0 - - if test -n "${PROMPT_COMMAND+_}"; then - eval "$PROMPT_COMMAND" - elif [ -n "${ZSH_VERSION-}" ]; then - update_hermit_env - fi - - return $res -} -` - func TestIntegration(t *testing.T) { tests := []struct { name string @@ -238,6 +198,13 @@ func TestIntegration(t *testing.T) { hermit manifest add-digests packages/testbin1.hcl assert grep d4f8989a4a6bf56ccc768c094448aa5f42be3b9f0287adc2f4dfd2241f80d2c0 packages/testbin1.hcl `}, + // Test that git sources with a specific reference are handled correctly. + {name: "SourceWithRef", + preparations: prep{unzip("git-source.zip", "source.git"), serveDir("source.git")}, + script: ` + hermit init --sources="file://$PWD/source.git#c0551672d8d179b93615e9612deaa2c3cc4fe0b5" + assert fail ./bin/hermit info testbin1-1.0.1 + `}, } checkForShells(t) @@ -354,6 +321,54 @@ func TestIntegration(t *testing.T) { } } +var shells = [][]string{ + {"bash", "--norc", "--noprofile"}, + {"zsh", "--no-rcs", "--no-globalrcs"}, +} + +// Functions that test scripts can use to communicate back to the test framework. +const preamble = ` +set -euo pipefail + +hermit-send() { + echo "$@" 1>&3 +} + +assert() { + if [ "${1:-}" = "fail" ]; then + shift + if "$@"; then + hermit-send "error: assertion should have failed: $@" + exit 1 + fi + elif ! "$@"; then + hermit-send "error: assertion failed: $@" + exit 1 + fi +} + +# Run a shell command and emulate what the Hermit shell hooks would do. +# +# usage: with-prompt-hooks +# +# Normally this is done by shell hooks, but because we're not running interactively this is not possible. +with-prompt-hooks() { + "$@" + res=$? + # We need to reset the change timestamp, as file timestamps are at second resolution. + # Some IT updates could be lost without this + export HERMIT_BIN_CHANGE=0 + + if test -n "${PROMPT_COMMAND+_}"; then + eval "$PROMPT_COMMAND" + elif [ -n "${ZSH_VERSION-}" ]; then + update_hermit_env + fi + + return $res +} +` + // Build Hermit from source. func buildAndInjectHermit(t *testing.T, environ []string) (outenviron []string) { t.Helper() @@ -527,3 +542,80 @@ func outputContains(text string) expectation { assert.Contains(t, output, text, "%s", output) } } + +// Unzip zipFile relative to testdata, into the test directory at dest. +func unzip(zipFile, dest string) preparation { + return func(t *testing.T, dir string) string { + t.Helper() + r, err := zip.OpenReader(filepath.Join("testdata", zipFile)) + assert.NoError(t, err) + defer r.Close() + for _, f := range r.File { + path := filepath.Join(dir, dest, f.Name) + if f.FileInfo().IsDir() { + err = os.MkdirAll(path, f.Mode()) + continue + } + + rc, err := f.Open() + assert.NoError(t, err) + err = os.MkdirAll(filepath.Dir(path), 0700) + assert.NoError(t, err) + w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, f.Mode()) + assert.NoError(t, err) + _, err = io.Copy(w, rc) + err = w.Close() + assert.NoError(t, err) + err = rc.Close() + assert.NoError(t, err) + } + return "" + } +} + +// Serve a directory on 127.0.0.1:8999 +func serveDir(httpRoot string) preparation { + return func(t *testing.T, dir string) string { + srv := &http.Server{ + Addr: "127.0.0.1:8999", + Handler: http.FileServer(http.Dir(filepath.Join(dir, httpRoot))), + } + go func() { _ = srv.ListenAndServe() }() + t.Cleanup(func() { _ = srv.Close() }) + return "" + } +} + +// Serve the given zip file on 127.0.0.1:8999 +func serveZip(zipFile string) preparation { + return func(t *testing.T, dir string) string { + t.Helper() + zr, err := zip.OpenReader(zipFile) + assert.NoError(t, err) + files := map[string]*zip.File{} + for _, file := range zr.File { + files[file.Name] = file + } + srv := &http.Server{ + Addr: "127.0.0.1:8999", + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := strings.TrimPrefix(r.URL.Path, "/") + file, ok := files[path] + if !ok { + http.NotFound(w, r) + return + } + w.Header().Add("Content-Type", "application/octet-stream") + w.Header().Add("Content-Size", fmt.Sprintf("%d", file.UncompressedSize64)) + rc, err := file.Open() + assert.NoError(t, err) + defer rc.Close() + _, err = io.Copy(w, rc) + assert.NoError(t, err) + }), + } + go func() { _ = srv.ListenAndServe() }() + t.Cleanup(func() { _ = srv.Close() }) + return "" + } +} diff --git a/integration/testdata/git-source.zip b/integration/testdata/git-source.zip new file mode 100644 index 0000000000000000000000000000000000000000..145c768b01889a47d9f8efb42413c85ecb710d3d GIT binary patch literal 18708 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 literal 0 HcmV?d00001 diff --git a/manifest/config.go b/manifest/config.go index 1f3aa3064..1c40ace2f 100644 --- a/manifest/config.go +++ b/manifest/config.go @@ -38,7 +38,7 @@ type Layer struct { Test *string `hcl:"test,optional" help:"Command that will test the package is operational."` Env envars.Envars `hcl:"env,optional" help:"Environment variables to export."` Vars map[string]string `hcl:"vars,optional" help:"Set local variables used during manifest evaluation."` - Source string `hcl:"source,optional" help:"URL for source package. Valid URLs are Git repositories (using .git[#] suffix), Local Files (using file:// prefix), and Remote Files (using http:// or https:// prefix)"` + Source string `hcl:"source,optional" help:"URL for source package. Valid URLs are Git repositories (using .git[#] suffix), Local Files (using file:// prefix), and Remote Files (using http:// or https:// prefix)"` DontExtract bool `hcl:"dont-extract,optional" help:"Don't extract the package source, just copy it into the installation directory."` Mirrors []string `hcl:"mirrors,optional" help:"Mirrors to use if the primary source is unavailable."` SHA256 string `hcl:"sha256,optional" help:"SHA256 of source package for verification. When in conflict with SHA256 in sha256sums, this value takes precedence."` diff --git a/sources/git.go b/sources/git.go index 43f75420b..93dd040fa 100644 --- a/sources/git.go +++ b/sources/git.go @@ -95,7 +95,8 @@ func syncGit(b *ui.Task, dir, source, finalDest string, runner util.CommandRunne return errors.WithStack(err) } defer os.RemoveAll(dest) - if err = runner.RunInDir(b, dest, "git", "clone", "--depth=1", source, dest); err != nil { + _, err = util.GitClone(b, runner, source, dest) + if err != nil { return errors.WithStack(err) } _ = os.RemoveAll(finalDest) @@ -103,6 +104,5 @@ func syncGit(b *ui.Task, dir, source, finalDest string, runner util.CommandRunne if err = os.Rename(dest, finalDest); err != nil && !os.IsExist(err) { // Prevent races. return errors.WithStack(err) } - return nil } diff --git a/sources/git_test.go b/sources/git_test.go index 4ce3f3eb7..e7d6006f8 100644 --- a/sources/git_test.go +++ b/sources/git_test.go @@ -8,12 +8,19 @@ import ( "github.com/cashapp/hermit/errors" "github.com/cashapp/hermit/sources" "github.com/cashapp/hermit/ui" + "github.com/cashapp/hermit/util" ) type FailingGit struct { err error } +var _ util.CommandRunner = &FailingGit{} + +func (f *FailingGit) CaptureInDir(log ui.Logger, dir string, args ...string) ([]byte, error) { + return nil, f.err +} + func (f *FailingGit) RunInDir(_ *ui.Task, _ string, _ ...string) error { return f.err } diff --git a/sources/sources.go b/sources/sources.go index db0ebe5b7..83072b6cd 100644 --- a/sources/sources.go +++ b/sources/sources.go @@ -1,7 +1,6 @@ package sources import ( - "github.com/cashapp/hermit/util" "io/fs" "net/url" "os" @@ -9,6 +8,8 @@ import ( "strings" "time" + "github.com/cashapp/hermit/util" + "github.com/cashapp/hermit/errors" "github.com/cashapp/hermit/ui" ) @@ -90,7 +91,7 @@ func getSource(b *ui.UI, source, dir, env string) (Source, error) { task := b.Task(source) defer task.Done() - if strings.HasSuffix(source, ".git") { + if strings.HasSuffix(source, ".git") || strings.Contains(source, ".git#") { return NewGitSource(source, dir, &util.RealCommandRunner{}), nil } diff --git a/util/git.go b/util/git.go new file mode 100644 index 000000000..ce91c464f --- /dev/null +++ b/util/git.go @@ -0,0 +1,44 @@ +package util + +import ( + "strings" + + "github.com/cashapp/hermit/errors" + "github.com/cashapp/hermit/ui" +) + +// GitClone clones a git repository and optionally checks out a ref, if specified (via #). +func GitClone(task *ui.Task, runner CommandRunner, url, checkoutDir string) (head string, err error) { + repo, ref := ParseGitURL(url) + args := []string{"git", "clone"} + if ref == "" { + args = append(args, "--depth=1") + } + args = append(args, repo, checkoutDir) + err = runner.RunInDir(task, ".", args...) + if err != nil { + return "", errors.WithStack(err) + } + if ref != "" { + task.Infof("%s: checking out %s", url, ref) + err = runner.RunInDir(task, checkoutDir, "git", "reset", "--hard", ref) + if err != nil { + return "", errors.WithStack(err) + } + } + bts, err := runner.CaptureInDir(task, checkoutDir, "git", "rev-parse", "HEAD") + if err != nil { + return "", errors.WithStack(err) + } + return strings.Trim(string(bts), "\n"), nil +} + +// ParseGitURL into a repo and an optional #ref. +func ParseGitURL(source string) (repo, ref string) { + parts := strings.SplitN(source, "#", 2) + repo = parts[0] + if len(parts) > 1 { + ref = parts[1] + } + return +} diff --git a/cache/source_test.go b/util/git_test.go similarity index 68% rename from cache/source_test.go rename to util/git_test.go index ac56400d5..3af303f35 100644 --- a/cache/source_test.go +++ b/util/git_test.go @@ -1,4 +1,4 @@ -package cache +package util import ( "testing" @@ -7,10 +7,10 @@ import ( ) func TestGitParseRepo(t *testing.T) { - repo, tag := parseGitURL("org-49461806@github.com:squareup/orc.git") + repo, tag := ParseGitURL("org-49461806@github.com:squareup/orc.git") assert.Equal(t, "org-49461806@github.com:squareup/orc.git", repo) assert.Equal(t, "", tag) - repo, tag = parseGitURL("org-49461806@github.com:squareup/orc.git#v1.2.3") + repo, tag = ParseGitURL("org-49461806@github.com:squareup/orc.git#v1.2.3") assert.Equal(t, "org-49461806@github.com:squareup/orc.git", repo) assert.Equal(t, "v1.2.3", tag) } diff --git a/util/run.go b/util/run.go index cc3316d46..105891cd6 100644 --- a/util/run.go +++ b/util/run.go @@ -16,11 +16,21 @@ import ( type CommandRunner interface { // RunInDir runs a command in the given directory. RunInDir(log *ui.Task, dir string, args ...string) error + // CaptureInDir runs a command in the given dir, returning combined stdout and stderr. + CaptureInDir(log ui.Logger, dir string, args ...string) ([]byte, error) } // RealCommandRunner actually calls command type RealCommandRunner struct{} +var _ CommandRunner = &RealCommandRunner{} + +// CaptureInDir implements CommandRunner +func (*RealCommandRunner) CaptureInDir(log ui.Logger, dir string, args ...string) ([]byte, error) { + data, err := CaptureInDir(log, dir, args...) + return data, errors.WithStack(err) +} + func (g *RealCommandRunner) RunInDir(task *ui.Task, dir string, commands ...string) error { // nolint: golint return errors.WithStack(RunInDir(task, dir, commands...)) }