diff --git a/src/BUILD_ b/src/BUILD_ index ced68310f2..87b0630549 100644 --- a/src/BUILD_ +++ b/src/BUILD_ @@ -7,6 +7,7 @@ go_binary( '//src/clean', '//src/cli', '//src/core', + '//src/export', '//src/gc', '//src/help', '//src/metrics', diff --git a/src/core/utils.go b/src/core/utils.go index 3d4dc5888a..efb834b20f 100644 --- a/src/core/utils.go +++ b/src/core/utils.go @@ -173,6 +173,10 @@ func RecursiveCopyFile(from string, to string, mode os.FileMode, link, fallback if fi.IsDir() { return RecursiveCopyFile(name+"/", dest+"/", mode, link, fallback) } else { + // 0 indicates inheriting the existing mode bits. + if mode == 0 { + mode = info.Mode() + } return copyOrLinkFile(name, dest, mode, link, fallback) } } else { diff --git a/src/export/BUILD b/src/export/BUILD new file mode 100644 index 0000000000..d6a66f5b58 --- /dev/null +++ b/src/export/BUILD @@ -0,0 +1,10 @@ +go_library( + name = 'export', + srcs = ['export.go'], + deps = [ + '//src/core', + '//src/gc', + '//third_party/go:logging', + ], + visibility = ['PUBLIC'], +) diff --git a/src/export/export.go b/src/export/export.go new file mode 100644 index 0000000000..c28ab440e8 --- /dev/null +++ b/src/export/export.go @@ -0,0 +1,75 @@ +// Package export handles exporting parts of the repo to other directories. +// This is useful if, for example, one wanted to separate out part of +// their repo with all dependencies. +package export + +import ( + "path" + "strings" + + "gopkg.in/op/go-logging.v1" + + "core" + "gc" +) + +var log = logging.MustGetLogger("export") + +// ToDir exports a set of targets to the given directory. +// It dies on any errors. +func ToDir(state *core.BuildState, dir string, targets []core.BuildLabel) { + done := map[*core.BuildTarget]bool{} + for _, target := range targets { + export(state.Graph, dir, state.Graph.TargetOrDie(target), done) + } + // Now write all the build files + packages := map[*core.Package]bool{} + for target := range done { + packages[state.Graph.PackageOrDie(target.Label.PackageName)] = true + } + for pkg := range packages { + dest := path.Join(dir, pkg.Filename) + if err := core.RecursiveCopyFile(pkg.Filename, dest, 0, false, false); err != nil { + log.Fatalf("Failed to copy BUILD file: %s\n", pkg.Filename) + } + // Now rewrite the unused targets out of it + victims := []string{} + for name, target := range pkg.Targets { + if !done[target] { + victims = append(victims, name) + } + } + if err := gc.RewriteFile(state, dest, victims); err != nil { + log.Fatalf("Failed to rewrite BUILD file: %s\n", err) + } + } +} + +// export implements the logic of ToDir, but prevents repeating targets. +func export(graph *core.BuildGraph, dir string, target *core.BuildTarget, done map[*core.BuildTarget]bool) { + if done[target] { + return + } + for _, src := range target.AllSources() { + if src.Label() == nil { // We'll handle these dependencies later + for _, p := range src.FullPaths(graph) { + if !strings.HasPrefix(p, "/") { // Don't copy system file deps. + if err := core.RecursiveCopyFile(p, path.Join(dir, p), 0, false, false); err != nil { + log.Fatalf("Error copying file: %s\n", err) + } + } + } + } + } + done[target] = true + for _, dep := range target.Dependencies() { + if parent := dep.Parent(graph); parent != nil && parent != target.Parent(graph) && parent != target { + export(graph, dir, parent, done) + } else { + export(graph, dir, dep, done) + } + } + for _, subinclude := range graph.PackageOrDie(target.Label.PackageName).Subincludes { + export(graph, dir, graph.TargetOrDie(subinclude), done) + } +} diff --git a/src/gc/gc.go b/src/gc/gc.go index 65d8da29c9..de34d09702 100644 --- a/src/gc/gc.go +++ b/src/gc/gc.go @@ -201,8 +201,11 @@ func publicDependencies(graph *core.BuildGraph, target *core.BuildTarget) []*cor return ret } -// rewriteFile rewrites a BUILD file to exclude a set of targets. -func rewriteFile(state *core.BuildState, filename string, targets []string) error { +// RewriteFile rewrites a BUILD file to exclude a set of targets. +func RewriteFile(state *core.BuildState, filename string, targets []string) error { + for i, t := range targets { + targets[i] = fmt.Sprintf(`"%s"`, t) + } data := string(MustAsset("rewrite.py")) // Template in the variables we want. data = strings.Replace(data, "__FILENAME__", filename, 1) @@ -214,12 +217,12 @@ func rewriteFile(state *core.BuildState, filename string, targets []string) erro func removeTargets(state *core.BuildState, labels core.BuildLabels) error { byPackage := map[string][]string{} for _, l := range labels { - byPackage[l.PackageName] = append(byPackage[l.PackageName], `"`+l.Name+`"`) + byPackage[l.PackageName] = append(byPackage[l.PackageName], l.Name) } for pkgName, victims := range byPackage { filename := state.Graph.PackageOrDie(pkgName).Filename - log.Notice("Rewriting %s to remove %s...\n", filename, strings.Replace(strings.Join(victims, ", "), `"`, "", -1)) - if err := rewriteFile(state, filename, victims); err != nil { + log.Notice("Rewriting %s to remove %s...\n", filename, strings.Join(victims, ", ")) + if err := RewriteFile(state, filename, victims); err != nil { return err } } diff --git a/src/gc/rewrite_test.go b/src/gc/rewrite_test.go index c33c9fd341..6e7b7912c7 100644 --- a/src/gc/rewrite_test.go +++ b/src/gc/rewrite_test.go @@ -31,7 +31,7 @@ func TestRewriteFile(t *testing.T) { wd, _ := os.Getwd() err := core.CopyFile("src/gc/test_data/before.build", path.Join(wd, "test.build"), 0644) assert.NoError(t, err) - assert.NoError(t, rewriteFile(state, "test.build", []string{`"prometheus"`, `"cover"`})) + assert.NoError(t, RewriteFile(state, "test.build", []string{"prometheus", "cover"})) rewritten, err := ioutil.ReadFile("test.build") assert.NoError(t, err) after, err := ioutil.ReadFile("src/gc/test_data/after.build") diff --git a/src/gc/stub.go b/src/gc/stub.go index 6e9f79000d..644a95333b 100644 --- a/src/gc/stub.go +++ b/src/gc/stub.go @@ -5,3 +5,6 @@ import "core" // GarbageCollect is a stub used at initial bootstrap time to avoid requiring us to run go-bindata yet again. func GarbageCollect(state *core.BuildState, filter, targets []core.BuildLabel, keepLabels []string, conservative, targetsOnly, srcsOnly, noPrompt, dryRun, git bool) { } + +// RewriteFile is also a stub used at boostrap time that does nothing. +func RewriteFile(state *core.BuildState, filename string, targets []string) error { return nil } diff --git a/src/please.go b/src/please.go index a8c025cd40..31e1106ebe 100644 --- a/src/please.go +++ b/src/please.go @@ -18,6 +18,7 @@ import ( "clean" "cli" "core" + "export" "gc" "help" "metrics" @@ -167,6 +168,13 @@ var opts struct { } `positional-args:"true"` } `command:"gc" description:"Analyzes the repo to determine unneeded targets."` + Export struct { + Output string `short:"o" long:"output" required:"true" description:"Directory to export into"` + Args struct { + Targets []core.BuildLabel `positional-arg-name:"targets" description:"Targets to export."` + } `positional-args:"true"` + } `command:"export" description:"Exports a set of targets and files from the repo."` + Help struct { Args struct { Topic string `positional-arg-name:"topic" description:"Topic to display help on"` @@ -346,6 +354,13 @@ var buildFunctions = map[string]func() bool{ } return success }, + "export": func() bool { + success, state := runBuild(opts.Export.Args.Targets, false, false) + if success { + export.ToDir(state, opts.Export.Output, state.ExpandOriginalTargets()) + } + return success + }, "help": func() bool { return help.Help(opts.Help.Args.Topic) },