From a0ece14e6e34d7c64f95585859690e1bd1e1a32f Mon Sep 17 00:00:00 2001 From: Dan Duvall Date: Mon, 7 Aug 2017 17:12:58 -0700 Subject: Use real types for build instructions Summary: Refactored build instructions to use concrete types and `build.Instruction` as an interface instead of relying on a simple enum and arbitrary string arguments. The formal types result in: 1. Clearer internal data structures 2. Partial compilation and proper argument quoting for all instructions moved into the common `build` package 3. Higher order instructions like `build.RunAll` that easily reduce to compiler specific output Test Plan: Run `arc unit` or `go test ./...` Reviewers: thcipriani, mmodell, #release-engineering-team Reviewed By: thcipriani, #release-engineering-team Tags: #release-engineering-team Differential Revision: https://phabricator.wikimedia.org/D741 --- build/instructions.go | 98 ++++++++++++++++++++++++++++++++++++++++++---- build/instructions_test.go | 51 ++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 build/instructions_test.go (limited to 'build') diff --git a/build/instructions.go b/build/instructions.go index e807b5b..5c12796 100644 --- a/build/instructions.go +++ b/build/instructions.go @@ -1,14 +1,96 @@ package build -type InstructionType int - -const ( - Run InstructionType = iota - Copy - Env +import ( + "fmt" + "sort" + "strconv" + "strings" ) -type Instruction struct { - Type InstructionType +type Instruction interface { + Compile() []string +} + +type Run struct { + Command string Arguments []string } + +func (run Run) Compile() []string { + numInnerArgs := strings.Count(run.Command, `%`) - strings.Count(run.Command, `%%`) + command := sprintf(run.Command, run.Arguments[0:numInnerArgs]) + + if len(run.Arguments) > numInnerArgs { + command += " " + strings.Join(quoteAll(run.Arguments[numInnerArgs:]), " ") + } + + return []string{command} +} + +type RunAll struct { + Runs []Run +} + +func (runAll RunAll) Compile() []string { + commands := make([]string, len(runAll.Runs)) + + for i, run := range runAll.Runs { + commands[i] = run.Compile()[0] + } + + return []string{strings.Join(commands, " && ")} +} + +type Copy struct { + Sources []string + Destination string +} + +func (copy Copy) Compile() []string { + return append(quoteAll(copy.Sources), quote(copy.Destination)) +} + +type Env struct { + Definitions map[string]string +} + +func (env Env) Compile() []string { + defs := make([]string, 0, len(env.Definitions)) + names := make([]string, 0, len(env.Definitions)) + + for name := range env.Definitions { + names = append(names, name) + } + + sort.Strings(names) + + for _, name := range names { + defs = append(defs, name+"="+quote(env.Definitions[name])) + } + + return defs +} + +func quote(arg string) string { + return strconv.Quote(arg) +} + +func quoteAll(arguments []string) []string { + quoted := make([]string, len(arguments)) + + for i, arg := range arguments { + quoted[i] = quote(arg) + } + + return quoted +} + +func sprintf(format string, arguments []string) string { + args := make([]interface{}, len(arguments)) + + for i, v := range arguments { + args[i] = quote(v) + } + + return fmt.Sprintf(format, args...) +} diff --git a/build/instructions_test.go b/build/instructions_test.go new file mode 100644 index 0000000..dbab4b5 --- /dev/null +++ b/build/instructions_test.go @@ -0,0 +1,51 @@ +package build_test + +import ( + "testing" + + "gopkg.in/stretchr/testify.v1/assert" + + "phabricator.wikimedia.org/source/blubber.git/build" +) + +func TestRun(t *testing.T) { + i := build.Run{"echo %s %s", []string{"foo bar", "baz"}} + + assert.Equal(t, []string{`echo "foo bar" "baz"`}, i.Compile()) +} + +func TestRunWithInnerAndOuterArguments(t *testing.T) { + i := build.Run{"useradd -d %s -u %s", []string{"/foo", "666", "bar"}} + + assert.Equal(t, []string{`useradd -d "/foo" -u "666" "bar"`}, i.Compile()) +} + +func TestRunAll(t *testing.T) { + i := build.RunAll{[]build.Run{ + {"echo %s", []string{"foo"}}, + {"cat %s", []string{"/bar"}}, + {"baz", []string{}}, + }} + + assert.Equal(t, []string{`echo "foo" && cat "/bar" && baz`}, i.Compile()) +} + +func TestCopy(t *testing.T) { + i := build.Copy{[]string{"source1", "source2"}, "dest"} + + assert.Equal(t, []string{`"source1"`, `"source2"`, `"dest"`}, i.Compile()) +} + +func TestEnv(t *testing.T) { + i := build.Env{map[string]string{ + "fooname": "foovalue", + "barname": "barvalue", + "quxname": "quxvalue", + }} + + assert.Equal(t, []string{ + `barname="barvalue"`, + `fooname="foovalue"`, + `quxname="quxvalue"`, + }, i.Compile()) +} -- cgit v1.2.1