diff --git a/docs/commands.html b/docs/commands.html
index e1ec8f8a3b..57988606da 100644
--- a/docs/commands.html
+++ b/docs/commands.html
@@ -801,6 +801,11 @@
whatoutputs: Prints out target(s) responsible for outputting provided file(s)
+
+
+ metadata: Prints out a structured, tree-like visualization of parsed build statement metadata (required subincludes, files, and generated targets) of a package.
+
+
@@ -1032,15 +1037,6 @@
Disables trimming unnecessary targets from exported packages. Normally targets in exported packages that
aren't dependencies of the originally exported targets are removed.
-
- This trimming syntax based, so doesn't always work depending on how the build definition is authored. Passing
- this flag will disable this feature, avoiding cases where these rules will be erroneously trimmed.
-
-
- To make sure a rule works without this flag, the rule must follow the naming convention, whereby children of
- :name follow the format :_name#{some-tag}. This is the
- format tag(name, tag) would produce.
-
diff --git a/src/cli/logging.go b/src/cli/logging.go
index 673ba92f0a..41547e0b22 100644
--- a/src/cli/logging.go
+++ b/src/cli/logging.go
@@ -13,7 +13,7 @@ import (
"sync"
cli "github.com/peterebden/go-cli-init/v5/logging"
- "github.com/peterebden/go-deferred-regex"
+ deferredregex "github.com/peterebden/go-deferred-regex"
"golang.org/x/term"
"gopkg.in/op/go-logging.v1"
@@ -184,9 +184,6 @@ func (backend *LogBackend) calcOutput() []string {
ret = append(ret, new...)
}
}
- if len(ret) > 0 {
- ret = append(ret, "Messages:")
- }
return reverse(ret)
}
@@ -248,6 +245,16 @@ func (backend *LogBackend) Output() []string {
return backend.output[:]
}
+// FlushOutput returns the current set of preformatted log messages and clears them.
+func (backend *LogBackend) FlushOutput() []string {
+ backend.mutex.Lock()
+ defer backend.mutex.Unlock()
+ ret := backend.output[:]
+ backend.output = nil
+ backend.logMessages.Init()
+ return ret
+}
+
// Wraps a string across multiple lines. Returned slice is reversed.
func (backend *LogBackend) lineWrap(msg string) []string {
lines := strings.Split(msg, "\n")
diff --git a/src/core/build_label.go b/src/core/build_label.go
index 779e7302e0..365e851c8c 100644
--- a/src/core/build_label.go
+++ b/src/core/build_label.go
@@ -473,7 +473,8 @@ func subrepoLabel(subrepoName, arch string) BuildLabel {
return BuildLabel{Name: subrepoName, Subrepo: arch}
}
-func hashBuildLabel(l BuildLabel) uint64 {
+// HashBuildLabel calculates an hash of Build Label, suitable to use for map indexing.
+func HashBuildLabel(l BuildLabel) uint64 {
return cmap.XXHashes(l.Subrepo, l.PackageName, l.Name)
}
@@ -622,3 +623,15 @@ func (slice BuildLabels) String() string {
}
return strings.Join(s, ", ")
}
+
+// labelSet defines a set of labels implemented using a map.
+type labelSet map[BuildLabel]struct{}
+
+func (ls labelSet) Add(l BuildLabel) {
+ ls[l] = struct{}{}
+}
+
+func (ls labelSet) Contains(l BuildLabel) bool {
+ _, ok := ls[l]
+ return ok
+}
diff --git a/src/core/build_target.go b/src/core/build_target.go
index 6fe7c01d0d..73d5130e29 100644
--- a/src/core/build_target.go
+++ b/src/core/build_target.go
@@ -1473,7 +1473,7 @@ func (target *BuildTarget) AllTestTools() []BuildInput {
if target.Test.namedTools == nil {
return target.Test.tools
}
- return target.allBuildInputs(target.Test.tools, target.Test.namedTools)
+ return combinedBuildInputs(target.Test.tools, target.Test.namedTools)
}
// NamedTestTools returns all named test tools
@@ -1489,7 +1489,7 @@ func (target *BuildTarget) AllDebugTools() []BuildInput {
if target.Debug.namedTools == nil {
return target.Debug.tools
}
- return target.allBuildInputs(target.Debug.tools, target.Debug.namedTools)
+ return combinedBuildInputs(target.Debug.tools, target.Debug.namedTools)
}
// AddDatum adds a new item of data to the target.
@@ -1644,21 +1644,41 @@ func (target *BuildTarget) getCommand(state *BuildState, commands map[string]str
return highestCommand
}
+// AllBuildInputs returns all the inputs for this target.
+func (target *BuildTarget) AllBuildInputs() []BuildInput {
+ srcs := target.AllSources()
+ data := target.AllData()
+ tools := target.AllTools()
+
+ size := len(srcs) + len(data) + len(tools)
+ inputs := make([]BuildInput, 0, size)
+ inputs = append(inputs, srcs...)
+ inputs = append(inputs, data...)
+ inputs = append(inputs, tools...)
+ return inputs
+}
+
// AllSources returns all the sources of this rule.
func (target *BuildTarget) AllSources() []BuildInput {
if target.NamedSources == nil {
return target.Sources
}
- return target.allBuildInputs(target.Sources, target.NamedSources)
+ return combinedBuildInputs(target.Sources, target.NamedSources)
}
-func (target *BuildTarget) allBuildInputs(unnamed []BuildInput, named map[string][]BuildInput) []BuildInput {
- ret := unnamed
+// combinedBuildInputs combines the unnamed inputs and the values of named inputs into one slice.
+func combinedBuildInputs(unnamed []BuildInput, named map[string][]BuildInput) []BuildInput {
keys := make([]string, 0, len(named))
- for k := range named {
+ size := 0
+ for k, vals := range named {
keys = append(keys, k)
+ size += len(vals)
}
sort.Strings(keys)
+
+ size += len(unnamed)
+ ret := make([]BuildInput, 0, size)
+ ret = append(ret, unnamed...)
for _, k := range keys {
ret = append(ret, named[k]...)
}
@@ -1710,7 +1730,7 @@ func (target *BuildTarget) AllData() []BuildInput {
return target.Data
}
- return target.allBuildInputs(target.Data, target.NamedData)
+ return combinedBuildInputs(target.Data, target.NamedData)
}
// AllDebugData returns all the data for debugging this rule.
@@ -1721,7 +1741,7 @@ func (target *BuildTarget) AllDebugData() []BuildInput {
if target.Debug.namedData == nil {
return target.Debug.data
}
- return target.allBuildInputs(target.Debug.data, target.Debug.namedData)
+ return combinedBuildInputs(target.Debug.data, target.Debug.namedData)
}
// DebugData returns unnamed data for debugging this rule.
@@ -1745,7 +1765,7 @@ func (target *BuildTarget) AllTools() []BuildInput {
if target.namedTools == nil {
return target.Tools
}
- return target.allBuildInputs(target.Tools, target.namedTools)
+ return combinedBuildInputs(target.Tools, target.namedTools)
}
// ToolNames returns an ordered list of tool names.
diff --git a/src/core/graph.go b/src/core/graph.go
index f3f9705e9e..fff4e451fb 100644
--- a/src/core/graph.go
+++ b/src/core/graph.go
@@ -13,17 +13,6 @@ import (
"github.com/thought-machine/please/src/cmap"
)
-type labelSet map[BuildLabel]struct{}
-
-func (ls labelSet) add(l BuildLabel) {
- ls[l] = struct{}{}
-}
-
-func (ls labelSet) contains(l BuildLabel) bool {
- _, ok := ls[l]
- return ok
-}
-
// A BuildGraph contains all the loaded targets and packages and maintains their
// relationships, especially reverse dependencies which are calculated here.
type BuildGraph struct {
@@ -35,7 +24,7 @@ type BuildGraph struct {
subrepos *cmap.Map[string, *Subrepo]
// Subincludes that are subincluded by other subincludes
subincludeSubincludes map[BuildLabel]labelSet
- // Use a mutex as a labelSet isn't atomic. We need to guard against inserting as well as mutating the value.
+ // Use a mutex as a LabelSet isn't atomic. We need to guard against inserting as well as mutating the value.
subincMux sync.Mutex
}
@@ -165,7 +154,7 @@ func (graph *BuildGraph) PackageMap() map[string]*Package {
// NewGraph constructs and returns a new BuildGraph.
func NewGraph() *BuildGraph {
g := &BuildGraph{
- targets: cmap.New[BuildLabel, *BuildTarget](cmap.DefaultShardCount, hashBuildLabel),
+ targets: cmap.New[BuildLabel, *BuildTarget](cmap.DefaultShardCount, HashBuildLabel),
packages: cmap.New[packageKey, *Package](cmap.DefaultShardCount, hashPackageKey),
subrepos: cmap.New[string, *Subrepo](cmap.SmallShardCount, cmap.XXHash),
subincludeSubincludes: map[BuildLabel]labelSet{},
@@ -197,10 +186,10 @@ func (graph *BuildGraph) TransitiveSubincludes(l BuildLabel) []BuildLabel {
}
func (graph *BuildGraph) findTransitiveSubincludes(label BuildLabel, includes labelSet) {
- if includes.contains(label) {
+ if includes.Contains(label) {
return
}
- includes.add(label)
+ includes.Add(label)
for l := range graph.subincludeSubincludes[label] {
graph.findTransitiveSubincludes(l, includes)
}
@@ -215,5 +204,5 @@ func (graph *BuildGraph) RegisterTransitiveSubinclude(from, to BuildLabel) {
incs = labelSet{}
graph.subincludeSubincludes[from] = incs
}
- incs.add(to)
+ incs.Add(to)
}
diff --git a/src/core/package.go b/src/core/package.go
index 377022a482..2bca9a0160 100644
--- a/src/core/package.go
+++ b/src/core/package.go
@@ -34,23 +34,46 @@ type Package struct {
targets map[string]*BuildTarget
// Set of output files from rules.
Outputs map[string]*BuildTarget
+ // Includes metadata from parsing the package BUILD file.
+ Metadata PackageMetadata
// Protects access to above
mutex sync.RWMutex
}
-// NewPackage constructs a new package with the given name.
-func NewPackage(name string) *Package {
- return NewPackageSubrepo(name, "")
+// PackageOptions is a functional option type for configuring a new Package.
+type PackageOptions func(*Package)
+
+// WithPackageSubrepo returns a PackageOptions that sets the subrepo name for a new Package.
+func WithPackageSubrepo(name string) PackageOptions {
+ return func(p *Package) {
+ p.SubrepoName = name
+ }
}
-// NewPackageSubrepo constructs a new package with the given name and subrepo.
-func NewPackageSubrepo(name, subrepo string) *Package {
- return &Package{
- Name: name,
- SubrepoName: subrepo,
- targets: map[string]*BuildTarget{},
- Outputs: map[string]*BuildTarget{},
+// WithPackageMetadata returns a PackageOptions that enables tracking of
+// metadata (like statement positions and subinclude mappings) for the Package.
+// This is required for features like 'plz export'.
+func WithPackageMetadata() PackageOptions {
+ return func(p *Package) {
+ p.Metadata = newPackageMetadata()
+ }
+}
+
+// NewPackage constructs a new package with the given name, and enables additional features
+// given the PackageOptions provided.
+func NewPackage(name string, options ...PackageOptions) *Package {
+ pkg := &Package{
+ Name: name,
+ targets: map[string]*BuildTarget{},
+ Outputs: map[string]*BuildTarget{},
+ // Defaults to noop to avoid storing metadata for most operations
+ Metadata: newNoopPackageMetadata(),
+ }
+
+ for _, option := range options {
+ option(pkg)
}
+ return pkg
}
// Target returns the target with the given name, or nil if this package doesn't have one.
@@ -109,7 +132,7 @@ func (pkg *Package) AllSubincludes(graph *BuildGraph) []BuildLabel {
for _, s := range pkg.Subincludes {
for _, inc := range append(graph.TransitiveSubincludes(s), s) {
- includes.add(inc)
+ includes.Add(inc)
}
}
diff --git a/src/core/package_metadata.go b/src/core/package_metadata.go
new file mode 100644
index 0000000000..95d91e780a
--- /dev/null
+++ b/src/core/package_metadata.go
@@ -0,0 +1,434 @@
+package core
+
+import (
+ "cmp"
+ "fmt"
+ "maps"
+ "slices"
+ "sync"
+)
+
+// BuildStatement represents a parsed statement in a BUILD file.
+type BuildStatement interface {
+ // StartPos returns the starting byte position of the build statement.
+ StartPos() int
+ // EndPos returns the ending byte position of the build statement.
+ EndPos() int
+}
+
+// BuildStatements is a slice of BuildStatement that implements sort.Interface.
+type BuildStatements []BuildStatement
+
+func (s BuildStatements) Len() int { return len(s) }
+func (s BuildStatements) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s BuildStatements) Less(i, j int) bool { return s[i].StartPos() < s[j].StartPos() }
+
+// BuildStatementProvider defines a closure that generates new build statements.
+// It is used as an argument in PackageMetadata methods to defer evaluation, avoiding
+// unnecessary computation when using the no-op implementation.
+type BuildStatementProvider func() BuildStatement
+
+// SubincludesLabelProvider defines a closure that generates labels for a subinclude statement.
+// It is used as an argument in PackageMetadata methods to defer evaluation, avoiding
+// unnecessary computation when using the no-op implementation.
+type SubincludesLabelProvider func() BuildLabels
+
+// StatementMetadata represents all parsed metadata associated with a single statement.
+//
+// Note: This struct does not currently use an explicit read-write mutex for synchronization.
+// Please maintains a separation between the write phase, in this case the interpreter phase, where
+// package metadata is written sequentially on a single thread per package as guaranteed by
+// [*BuildState.SyncParsePackage] and a subsequent read phase where we support concurrency if the
+// metadata is treated as static and read-only.
+type StatementMetadata struct {
+ // Subincludes tracks the direct, package-level subinclude labels that were required for the
+ // successful interpretation of this build statement. This allows mapping the statement back
+ // to the subincluded files/symbols it depends on. For transitive subincludes, use
+ // [BuildGraph.TransitiveSubincludes] instead.
+ Subincludes BuildLabels
+ // Files tracks the file paths that were required during interpretation of this statement.
+ // These do not reference target sources (which are dependencies of build rules), but rather
+ // files evaluated directly during interpretation, such as files matched and captured by a
+ // glob() action.
+ Files []string
+ // Targets lists the build targets produced as a result of this statement. Since a single
+ // statement (such as a custom build rule, function call, or loop) can define multiple
+ // targets, this is represented as a list of build labels.
+ Targets BuildLabels
+ // IsSubincludeStatement tracks whether this build statement (identified by its position
+ // in the BUILD file) is a subinclude() call.
+ IsSubincludeStatement bool
+}
+
+// TrackedStatement pairs a build statement with its evaluated metadata.
+type TrackedStatement struct {
+ Statement BuildStatement
+ Metadata StatementMetadata
+}
+
+// PackageMetadata stores metadata about parsed BUILD files, mapping statements and subincludes
+// to their respective targets. This supports additional logic for operations such as `plz export`
+// but should be disabled for most operations by using the no-op implementation to avoid the overhead.
+type PackageMetadata interface {
+ // RegisterStatement records a statement of an interpreted BUILD file and its
+ // dependencies. Required subincludes identify the required subincluded targets for a successful
+ // interpretation of the statement, i.e. targets that provide the required symbols (variables or
+ // methods). The files argument identify the files required when interpreting that statement, for
+ // example when interpreting a glob() statement, this argument will include files captured by the
+ // file globbing action.
+ RegisterStatement(stmt BuildStatement, requiredSubincludes BuildLabels, files []string)
+ // RegisterTargetStatement records that the given build target was created as a result of the
+ // given statement being executed. For statements that generate targets, we expect this method
+ // to be called in addition to [PackageMetadata.RegisterStatement].
+ // This should only be called for statements in BUILD files.
+ RegisterTargetStatement(target BuildLabel, stmtProvider BuildStatementProvider)
+ // RegisterSubincludeStatement records the given build statement (provided by stmtProvider)
+ // as being a subinclude statement. We expect this method to be called in addition to
+ // [PackageMetadata.RegisterStatement]. This should only be called for statements in BUILD files.
+ RegisterSubincludeStatement(stmtProvider BuildStatementProvider)
+ // FindStatement returns the build statement that was responsible for generating the given target.
+ // Returns an error if the target was not found in the recorded metadata.
+ FindStatement(target BuildLabel) (BuildStatement, error)
+ // FindTargets returns all build targets that were generated by the given build statement.
+ // Returns an empty slice if no targets were found for the given statement.
+ FindTargets(stmt BuildStatement) BuildLabels
+ // FindRequiredSubincludes returns all subinclude labels that were required by the given target.
+ // This method will only report the package level (direct) subincludes, make use of
+ // [BuildGraph.TransitiveSubincludes] if you want all the required subincludes for one target.
+ FindRequiredSubincludes(target BuildLabel) (BuildLabels, error)
+ // FindRelatedTargets finds all the targets that are related to the argument. In this context,
+ // target relationship is determined by looking for targets generated by the same build statement.
+ // The result excludes the target in the argument.
+ FindRelatedTargets(target BuildLabel) (BuildLabels, error)
+ // FindPackageLevelRequirements finds all the subincluded labels required by the package that
+ // are not associated with BuildTarget generation. An example could be a variable declaration
+ // that depends on a subincluded value.
+ FindPackageLevelRequirements() (BuildLabels, []string)
+ // GetSubincludedLabels returns all build labels that were included by the given subinclude statement.
+ // Returns the labels or an empty slice if the statement wasn't found.
+ GetSubincludedLabels(stmt BuildStatement) BuildLabels
+ // IsInterpretedStatement returns true if the statement provided matches a registered build
+ // statement, meaning it was interpreted even if it doesn't generate any targets.
+ IsInterpretedStatement(stmt BuildStatement) bool
+ // Statements returns all build statement and their metadata tracked in the package, sorted by
+ // BUILD file order.
+ Statements() []TrackedStatement
+}
+
+// trackedPackageMetadata is the canonical implementation of the PackageMetadata interface. It tracks
+// the relationships between BUILD file statements, subincludes, and the build targets they define.
+type trackedPackageMetadata struct {
+ // statements maps each build statement (identified by its byte range in a BUILD file)
+ // to its StatementMetadata. Refer to [StatementMetadata] for more details but this single
+ // mapping tracks the targets produced by the statement, the subincluded labels required for its
+ // interpretation, and other information.
+ statements map[BuildStatement]*StatementMetadata
+ // targetToStmt serves as a reverse-lookup map, linking each generated BuildLabel
+ // back to the specific BuildStatement that declared it. This is useful for tracing back a target
+ // to its statement and to find sibling targets generated by the same statement block.
+ targetToStmt map[BuildLabel]BuildStatement
+ // mutex protects the entire [trackedPackageMetadata] from concurrent access.
+ //
+ // Note: We include a mutex to ensure consistency in case we use [trackedPackageMetadata] in a
+ // multi-threaded context, however, in the current implementation writes (interpreter phase) are
+ // performed on a single-thread per package. Please guarantees that each package's BUILD file and
+ // its subincludes are interpreted on exactly one parser thread at a time (enforced by
+ // [BuildState.SyncParsePackage]). Nevertheless we use [sync.RWMutex] to support future concurrent
+ // reads and the overhead should be minimal for single-threaded application, since there won't be
+ // any lock contention.
+ mutex sync.RWMutex
+}
+
+func newPackageMetadata() PackageMetadata {
+ return &trackedPackageMetadata{
+ statements: make(map[BuildStatement]*StatementMetadata),
+ targetToStmt: make(map[BuildLabel]BuildStatement),
+ }
+}
+
+// RegisterStatement implements [PackageMetadata.RegisterStatement].
+func (m *trackedPackageMetadata) RegisterStatement(stmt BuildStatement, requiredSubincludes BuildLabels, files []string) {
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+
+ sm, exists := m.statements[stmt]
+ if !exists {
+ sm = &StatementMetadata{}
+ m.statements[stmt] = sm
+ }
+
+ if len(requiredSubincludes) > 0 {
+ // It's necessary to support subsequent calls to this method due to dynamic subincludes.
+ // A function definition can include calls to subinclude() with a dynamic argument, so here
+ // we need to support appending to the required subincludes, meaning this method will be
+ // called more than once with different arguments. For an example refer to
+ // [test/export/test_dynamic_subinclude].
+ sm.Subincludes = mergeSlices(sm.Subincludes, requiredSubincludes)
+ }
+ if len(files) > 0 {
+ // Subsequent calls have to be supported for similar reasons to the above, appending the value.
+ sm.Files = mergeSlices(sm.Files, files)
+ }
+}
+
+// mergeSlices merges two slices of any comparable type. It de-duplicates elements using
+// slices.Contains so it should be used for small slices. A set/map approach should be preferred for
+// larger slices.
+func mergeSlices[T comparable](existing []T, newElements []T) []T {
+ merged := append([]T(nil), existing...)
+ for _, el := range newElements {
+ if !slices.Contains(merged, el) {
+ merged = append(merged, el)
+ }
+ }
+ return merged
+}
+
+// RegisterTargetStatement implements [PackageMetadata.RegisterTargetStatement].
+func (m *trackedPackageMetadata) RegisterTargetStatement(target BuildLabel, stmtProvider BuildStatementProvider) {
+ stmt := stmtProvider()
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+
+ sm, exists := m.statements[stmt]
+ if !exists {
+ sm = &StatementMetadata{}
+ m.statements[stmt] = sm
+ }
+
+ sm.Targets = append(sm.Targets, target)
+ m.targetToStmt[target] = stmt
+}
+
+// RegisterSubincludeStatement implements [PackageMetadata.RegisterSubincludeStatement].
+func (m *trackedPackageMetadata) RegisterSubincludeStatement(stmtProvider BuildStatementProvider) {
+ stmt := stmtProvider()
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+
+ sm, exists := m.statements[stmt]
+ if !exists {
+ sm = &StatementMetadata{}
+ m.statements[stmt] = sm
+ }
+ sm.IsSubincludeStatement = true
+}
+
+// FindStatement implements [PackageMetadata.FindStatement].
+func (m *trackedPackageMetadata) FindStatement(target BuildLabel) (BuildStatement, error) {
+ m.mutex.RLock()
+ defer m.mutex.RUnlock()
+
+ return m.findStatement(target)
+}
+
+func (m *trackedPackageMetadata) findStatement(target BuildLabel) (BuildStatement, error) {
+ stmt, ok := m.targetToStmt[target]
+ if !ok {
+ return nil, fmt.Errorf("failed to find statement for target %s", target)
+ }
+ return stmt, nil
+}
+
+// FindTargets implements [PackageMetadata.FindTargets].
+func (m *trackedPackageMetadata) FindTargets(stmt BuildStatement) BuildLabels {
+ m.mutex.RLock()
+ defer m.mutex.RUnlock()
+
+ return m.findTargets(stmt)
+}
+
+func (m *trackedPackageMetadata) findTargets(stmt BuildStatement) BuildLabels {
+ if sm, ok := m.statements[stmt]; ok {
+ return sm.Targets
+ }
+ return nil
+}
+
+// FindRequiredSubincludes implements [PackageMetadata.FindRequiredSubincludes].
+func (m *trackedPackageMetadata) FindRequiredSubincludes(target BuildLabel) (BuildLabels, error) {
+ m.mutex.RLock()
+ defer m.mutex.RUnlock()
+
+ stmt, err := m.findStatement(target)
+ if err != nil {
+ return nil, err
+ }
+
+ sm, ok := m.statements[stmt]
+ if !ok {
+ return nil, fmt.Errorf("failed to find metadata for statement %v", stmt)
+ }
+ if len(sm.Subincludes) == 0 {
+ // Could be empty if no subincluded label is required - a valid outcome hence the nil error.
+ return nil, nil
+ }
+ return sm.Subincludes, nil
+}
+
+// FindRelatedTargets implements [PackageMetadata.FindRelatedTargets].
+func (m *trackedPackageMetadata) FindRelatedTargets(target BuildLabel) (BuildLabels, error) {
+ m.mutex.RLock()
+ defer m.mutex.RUnlock()
+
+ stmt, err := m.findStatement(target)
+ if err != nil {
+ return nil, err
+ }
+
+ relatedTargets := m.findTargets(stmt)
+ labels := make(BuildLabels, 0, len(relatedTargets)-1) // -1 since we exclude the target argument
+ for _, l := range relatedTargets {
+ if l != target {
+ labels = append(labels, l)
+ }
+ }
+ return labels, nil
+}
+
+// FindPackageLevelRequirements implements [PackageMetadata.FindPackageLevelRequirements].
+func (m *trackedPackageMetadata) FindPackageLevelRequirements() (BuildLabels, []string) {
+ m.mutex.RLock()
+ defer m.mutex.RUnlock()
+
+ requiredSet := labelSet{}
+ filesSet := map[string]struct{}{}
+
+ // The intention is to finds all the subincluded labels required by the package but not used to
+ // generate targets. An example could be a variable declaration that depends on a subincluded value.
+ // We range over all interpreted statements that require any subincluded target. From those, we
+ // filter out the statements that generate targets and any explicit subinclude() statement calls.
+ // Similar logic for files that are included by a glob() statement.
+ for _, sm := range m.statements {
+ if len(sm.Targets) == 0 && !sm.IsSubincludeStatement {
+ for _, label := range sm.Subincludes {
+ requiredSet.Add(label)
+ }
+ for _, file := range sm.Files {
+ filesSet[file] = struct{}{}
+ }
+ }
+ }
+
+ subincludes := slices.Collect(maps.Keys(requiredSet))
+ slices.SortFunc(subincludes, BuildLabel.Compare)
+
+ files := slices.Collect(maps.Keys(filesSet))
+ slices.Sort(files)
+
+ return subincludes, files
+}
+
+// GetSubincludedLabels implements [PackageMetadata.GetSubincludedLabels].
+func (m *trackedPackageMetadata) GetSubincludedLabels(stmt BuildStatement) BuildLabels {
+ m.mutex.RLock()
+ defer m.mutex.RUnlock()
+
+ // After determining that this is a subincludes statement we can return the required subincludes
+ // registered in the general statement mapping.
+ if sm, ok := m.statements[stmt]; ok && sm.IsSubincludeStatement {
+ return sm.Subincludes
+ }
+ return nil
+}
+
+// IsInterpretedStatement implements [PackageMetadata.IsInterpretedStatement].
+func (m *trackedPackageMetadata) IsInterpretedStatement(stmt BuildStatement) bool {
+ m.mutex.RLock()
+ defer m.mutex.RUnlock()
+
+ _, exists := m.statements[stmt]
+ return exists
+}
+
+// Statements implements [PackageMetadata.Statements].
+func (m *trackedPackageMetadata) Statements() []TrackedStatement {
+ m.mutex.RLock()
+ defer m.mutex.RUnlock()
+
+ statements := make([]TrackedStatement, 0, len(m.statements))
+ for stmt, sm := range m.statements {
+ statements = append(statements, TrackedStatement{
+ Statement: stmt,
+ Metadata: *sm,
+ })
+ }
+
+ // Sort the statements to maintain BUILD file order
+ slices.SortFunc(statements, func(i, j TrackedStatement) int {
+ return cmp.Compare(i.Statement.StartPos(), j.Statement.StartPos())
+ })
+
+ return statements
+}
+
+// noopPackageMetadata implements the PackageMetadata interface with no-op methods. This is the
+// default implementation and is used to avoid the overhead of parsing metadata for operations that
+// don't depend on it. To ensure correctness and that the no-op implementation is not used
+// unintentionally, all methods which attempt to retrieve information from this no-op implementation
+// will cause Please to terminate.
+type noopPackageMetadata struct{}
+
+func newNoopPackageMetadata() PackageMetadata {
+ return &noopPackageMetadata{}
+}
+
+// RegisterStatement implements [PackageMetadata.RegisterStatement].
+func (n *noopPackageMetadata) RegisterStatement(stmt BuildStatement, requiredSubincludes BuildLabels, files []string) {
+}
+
+// RegisterTargetStatement implements [PackageMetadata.RegisterTargetStatement].
+func (n *noopPackageMetadata) RegisterTargetStatement(target BuildLabel, stmtProvider BuildStatementProvider) {
+}
+
+// RegisterSubincludeStatement implements [PackageMetadata.RegisterSubincludeStatement].
+func (n *noopPackageMetadata) RegisterSubincludeStatement(stmtProvider BuildStatementProvider) {
+}
+
+// FindStatement implements [PackageMetadata.FindStatement].
+func (n *noopPackageMetadata) FindStatement(target BuildLabel) (BuildStatement, error) {
+ log.Fatalf("metadata not tracked, using no-op implementation")
+ return nil, nil
+}
+
+// FindTargets implements [PackageMetadata.FindTargets].
+func (n *noopPackageMetadata) FindTargets(stmt BuildStatement) BuildLabels {
+ log.Fatalf("metadata not tracked, using no-op implementation")
+ return nil
+}
+
+// FindRequiredSubincludes implements [PackageMetadata.FindRequiredSubincludes].
+func (n *noopPackageMetadata) FindRequiredSubincludes(target BuildLabel) (BuildLabels, error) {
+ log.Fatalf("metadata not tracked, using no-op implementation")
+ return nil, nil
+}
+
+// FindRelatedTargets implements [PackageMetadata.FindRelatedTargets].
+func (n *noopPackageMetadata) FindRelatedTargets(target BuildLabel) (BuildLabels, error) {
+ log.Fatalf("metadata not tracked, using no-op implementation")
+ return nil, nil
+}
+
+// FindPackageLevelRequirements implements [PackageMetadata.FindPackageLevelRequirements].
+func (n *noopPackageMetadata) FindPackageLevelRequirements() (BuildLabels, []string) {
+ log.Fatalf("metadata not tracked, using no-op implementation")
+ return nil, nil
+}
+
+// GetSubincludedLabels implements [PackageMetadata.GetSubincludedLabels].
+func (n *noopPackageMetadata) GetSubincludedLabels(stmt BuildStatement) BuildLabels {
+ log.Fatalf("metadata not tracked, using no-op implementation")
+ return nil
+}
+
+// IsInterpretedStatement implements [PackageMetadata.IsInterpretedStatement].
+func (n *noopPackageMetadata) IsInterpretedStatement(stmt BuildStatement) bool {
+ log.Fatalf("metadata not tracked, using no-op implementation")
+ return false
+}
+
+// Statements implements [PackageMetadata.Statements].
+func (n *noopPackageMetadata) Statements() []TrackedStatement {
+ log.Fatalf("metadata not tracked, using no-op implementation")
+ return nil
+}
diff --git a/src/core/state.go b/src/core/state.go
index 2a9ca6fb0a..685c6184a7 100644
--- a/src/core/state.go
+++ b/src/core/state.go
@@ -240,6 +240,13 @@ type BuildState struct {
// NeedDebugDeps is true if we're doing a `plz debug` and we need to build the debug tools and
// data
NeedDebugDeps bool
+ // ParseMetadata is true if we want to store BUILD file metadata
+ ParseMetadata bool
+ // ForceParseEntirePackage is true if we want to force parse and activate all targets in every visited
+ // package during the parse phase. This is used by the export operation to ensure that the entire
+ // dependency graph of visited packages is parsed upfront, including co-defined related targets
+ // and sibling targets.
+ ForceParseEntirePackage bool
// initOnce is used to control loading the subrepo .plzconfig
initOnce *sync.Once
@@ -601,6 +608,23 @@ func (state *BuildState) LogTestResult(target *BuildTarget, run int, status Buil
// LogBuildError logs a failure for a target to parse, build or test.
func (state *BuildState) LogBuildError(label BuildLabel, status BuildResultStatus, err error, format string, args ...interface{}) {
+ if status == ParseFailed {
+ // Force close package wait channels to avoid deadlocks when calling waitForPackage() after
+ // the initial parse, for example when [state.ParseMetadata] is set.
+ key := packageKey{Name: label.PackageName, Subrepo: label.Subrepo}
+ if ch := state.progress.pendingPackages.Get(key); ch != nil {
+ func() {
+ defer func() { recover() }() // recover if attempted to close a closed channel.
+ close(ch) // This signals to anyone waiting that it's done (failed, but completed).
+ }()
+ }
+ if ch := state.progress.packageWaits.Get(key); ch != nil {
+ func() {
+ defer func() { recover() }() // recover if attempted to close a closed channel.
+ close(ch) // This signals to anyone waiting that it's done (failed, but completed).
+ }()
+ }
+ }
state.logResult(&BuildResult{
Label: label,
Status: status,
@@ -665,11 +689,17 @@ func (state *BuildState) forwardResults() {
delete(activeTargets, target)
}
}
- state.progress.mutex.Lock()
- if state.progress.results != nil {
- state.progress.results <- result
- }
- state.progress.mutex.Unlock()
+ state.sendResult(result)
+ }
+}
+
+// sendResult sends a unique result to the channel. A simple method that is mostly useful for a
+// deferring the mutex close and avoid deadlocks even when we attempt to write to a closed channel.
+func (state *BuildState) sendResult(result *BuildResult) {
+ state.progress.mutex.Lock()
+ defer state.progress.mutex.Unlock()
+ if state.progress.results != nil {
+ state.progress.results <- result
}
}
@@ -1065,6 +1095,23 @@ func (state *BuildState) ActivateTarget(pkg *Package, label, dependent BuildLabe
return nil
}
+// QueueEntirePackage queues and activates all targets in the specified package, except optionally a
+// target to skip. This is used when we want to ensure the entire package and its transitive
+// dependency graph are parsed upfront. Refer to [state.ForceParseEntirePackage].
+func (state *BuildState) QueueEntirePackage(pkg *Package, skip BuildLabel, dependent BuildLabel, mode ParseMode) error {
+ for _, target := range pkg.AllTargets() {
+ // Skip the designated target (usually the currently parsing one) and post-build targets.
+ // Similarly to [state.ActivateTarget] we skip queueing post-build targets due to potential race
+ // conditions with post-build function execution.
+ if target.Label != skip && !target.AddedPostBuild {
+ if err := state.QueueTarget(target.Label, dependent, false, mode); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
// exportFile adds a single-file export target. This is primarily used for Bazel compat.
func exportFile(state *BuildState, pkg *Package, label BuildLabel) {
t := NewBuildTarget(label)
@@ -1482,7 +1529,7 @@ func NewBuildState(config *Configuration) *BuildState {
progress: &stateProgress{
numActive: 1, // One for the initial target adding on the main thread.
numPending: 1,
- pendingTargets: cmap.New[BuildLabel, chan struct{}](cmap.DefaultShardCount, hashBuildLabel),
+ pendingTargets: cmap.New[BuildLabel, chan struct{}](cmap.DefaultShardCount, HashBuildLabel),
pendingPackages: cmap.New[packageKey, chan struct{}](cmap.DefaultShardCount, hashPackageKey),
packageWaits: cmap.New[packageKey, chan struct{}](cmap.DefaultShardCount, hashPackageKey),
internalResults: make(chan *BuildResult, 1000),
diff --git a/src/core/state_test.go b/src/core/state_test.go
index 1500de9971..7c076eaf53 100644
--- a/src/core/state_test.go
+++ b/src/core/state_test.go
@@ -1,6 +1,7 @@
package core
import (
+ "fmt"
"strings"
"sync"
"testing"
@@ -205,3 +206,63 @@ func TestWaitForPackageConcurrent(t *testing.T) {
}
}
}
+
+func TestLogBuildErrorConcurrency(t *testing.T) {
+ state := NewDefaultBuildState()
+ label := ParseBuildLabel("//src/core:all", "")
+
+ // Spawn multiple goroutines waiting for the package to finish parsing.
+ // These will call SyncParsePackage, and block on the channel for this package .
+ const numWaiters = 5
+ var wg sync.WaitGroup
+ started := make(chan int, numWaiters)
+ tasks := make(chan int, numWaiters)
+ for i := range numWaiters {
+ wg.Go(func() {
+ started <- i
+ state.SyncParsePackage(label)
+ tasks <- i
+ })
+ }
+
+ // Wait for all goroutines to start and be about to call SyncParsePackage.
+ for range numWaiters {
+ <-started
+ }
+ // Wait for the first call to finish. Exactly one of the calls shouldn't block.
+ <-tasks
+
+ // Give other waiters time to block on the wait channel.
+ time.Sleep(time.Millisecond)
+ if len(tasks) > 0 {
+ t.Errorf("Expected only the first caller of SyncParsePackage to proceed immediately, but %d others unblocked prematurely", len(tasks))
+ }
+
+ // Trigger failures for the exact same package. Concurrently to validate the logic for closing
+ // channels.
+ const numFailures = 3
+ var wgFail sync.WaitGroup
+ for range numFailures {
+ wgFail.Go(func() {
+ state.LogBuildError(label, ParseFailed, fmt.Errorf("intended test error"), "failed to parse %s", label)
+ })
+ }
+ wgFail.Wait()
+
+ // Ensure all blocked waiters are unblocked safely (no deadlocks)
+ done := make(chan struct{})
+ go func() {
+ wg.Wait()
+ close(done)
+ }()
+
+ select {
+ case <-done:
+ // All waiters unblocked without panicking or deadlocking.
+ if len(tasks) != numWaiters-1 {
+ t.Errorf("Expected all remaining 4 waiters to have unblocked and written to tasks, but only %d did", len(tasks))
+ }
+ case <-time.After(time.Second):
+ t.Fatal("Deadlock detected: waiting goroutines failed to unblock within 1 seconds")
+ }
+}
diff --git a/src/core/subrepo.go b/src/core/subrepo.go
index 5244226c08..100d4e0ab8 100644
--- a/src/core/subrepo.go
+++ b/src/core/subrepo.go
@@ -68,6 +68,11 @@ func (s *Subrepo) IsRemoteSubrepo() bool {
return s.Root != "" && s.Target != nil && !s.Target.Local && s.State.RemoteClient != nil
}
+// IsExternal returns true if this subrepo is external, meaning its sources are fetched by a build target.
+func (s *Subrepo) IsExternal() bool {
+ return s != nil && s.Target != nil
+}
+
// Equal returns true if this subrepo is equivalent to another, or false if it is not.
func (s *Subrepo) Equal(other *Subrepo) bool {
return s.Name == other.Name && s.Root == other.Root && s.PackageRoot == other.PackageRoot &&
diff --git a/src/export/BUILD b/src/export/BUILD
index 6b44d63d9b..5069f9dc85 100644
--- a/src/export/BUILD
+++ b/src/export/BUILD
@@ -1,13 +1,30 @@
go_library(
name = "export",
- srcs = ["export.go"],
+ srcs = glob(
+ ["*.go"],
+ exclude = ["*_test.go"],
+ ),
pgo_file = "//:pgo",
visibility = ["PUBLIC"],
deps = [
+ "///third_party/go/github.com_please-build_buildtools//build",
+ "//src/cli",
"//src/cli/logging",
"//src/core",
"//src/fs",
- "//src/gc",
"//src/parse",
+ "//src/parse/asp",
+ ],
+)
+
+go_test(
+ name = "export_test",
+ srcs = ["export_test.go"],
+ data = ["test_data"],
+ deps = [
+ ":export",
+ "///third_party/go/github.com_stretchr_testify//assert",
+ "//src/core",
+ "//src/parse/asp",
],
)
diff --git a/src/export/export.go b/src/export/export.go
index 2863dfef0c..b0e39bd6d5 100644
--- a/src/export/export.go
+++ b/src/export/export.go
@@ -4,212 +4,257 @@
package export
import (
- iofs "io/fs"
+ "fmt"
"os"
"path/filepath"
+ "time"
+ "github.com/thought-machine/please/src/cli"
"github.com/thought-machine/please/src/cli/logging"
"github.com/thought-machine/please/src/core"
"github.com/thought-machine/please/src/fs"
- "github.com/thought-machine/please/src/gc"
- "github.com/thought-machine/please/src/parse"
)
var log = logging.Log
-type export struct {
- state *core.BuildState
- targetDir string
- noTrim bool
-
- exportedTargets map[core.BuildLabel]bool
- exportedPackages map[string]bool
-}
-
-// ToDir exports a set of targets to the given directory.
-// It dies on any errors.
-func ToDir(state *core.BuildState, dir string, noTrim bool, targets []core.BuildLabel) {
- e := &export{
- state: state,
- noTrim: noTrim,
- targetDir: dir,
- exportedPackages: map[string]bool{},
- exportedTargets: map[core.BuildLabel]bool{},
- }
+// Repo export a new please repo including the targets and dependencies requested. Depending on the
+// noTrim flag, the export will attempt to trim the resulting repository, exporting only the required
+// targets and build statements in their packages. If noTrim is set, all targets of a package will be
+// exported and no build statement trimming will be attempted, the BUILD file is copied in its entirety.
+func Repo(state *core.BuildState, dir string, noTrim bool, targets []core.BuildLabel) {
+ e := newExporter(state, dir, noTrim)
+ // ensure output dir
if err := os.MkdirAll(dir, fs.DirPermissions); err != nil {
log.Fatalf("failed to create export directory %s: %v", dir, err)
}
- e.exportPlzConf()
- for _, target := range state.Config.Parse.PreloadSubincludes {
- for _, includeLabel := range append(state.Graph.TransitiveSubincludes(target), target) {
- e.export(state.Graph.TargetOrDie(includeLabel))
+ e.run(targets)
+}
+
+// Outputs exports the build artifacts (output files) produced by building the specified
+// targets to the given output directory.
+func Outputs(state *core.BuildState, dir string, targets []core.BuildLabel) {
+ for _, label := range targets {
+ target := state.Graph.TargetOrDie(label)
+ for _, out := range target.Outputs() {
+ fullPath := filepath.Join(dir, out)
+ outDir := filepath.Dir(fullPath)
+ if err := os.MkdirAll(outDir, core.DirPermissions); err != nil {
+ log.Fatalf("Failed to create export dir %s: %s", outDir, err)
+ }
+ if err := fs.RecursiveCopy(filepath.Join(target.OutDir(), out), fullPath, target.OutMode()|0200); err != nil {
+ log.Fatalf("Failed to copy export file: %s", err)
+ }
}
}
- for _, target := range targets {
- e.export(state.Graph.TargetOrDie(target))
- }
- // Now write all the build files
- packages := map[*core.Package]bool{}
- for target := range e.exportedTargets {
- packages[state.Graph.PackageOrDie(target)] = true
- }
+}
- // Write any preloaded build defs as well; preloaded subincludes should be fine though.
- for _, preload := range state.Config.Parse.PreloadBuildDefs {
- if err := fs.RecursiveCopy(preload, filepath.Join(dir, preload), 0); err != nil {
- log.Fatalf("Failed to copy preloaded build def %s: %s", preload, err)
- }
+// exporterImpl defines the interface for exporting parts of a Please repository to a new directory.
+// It handles the copying of configuration files, preloaded build definitions, and selected
+// targets along with their necessary source files and dependencies.
+type exporterImpl interface {
+ // exportPreloaded exports all globally preloaded build definitions and subincluded targets.
+ // These are usually defined in the repository's configuration file.
+ exportPreloaded()
+ // exportTarget exports an individual build target.
+ // Each target recursively exports all their source files and required build statements, but also
+ // targets in their transitive dependencies.
+ exportTarget(*core.BuildTarget)
+ // writePackageFiles writes the processed BUILD files for all exported targets to the
+ // export directory. These BUILD files may be modified (e.g., trimmed) depending on
+ // the exporter's implementation.
+ writePackageFiles()
+}
+
+// newExporter creates a new exporter of a specific type based on the arguments.
+func newExporter(state *core.BuildState, dir string, noTrim bool) *baseExporter {
+ base := &baseExporter{
+ state: state,
+ targetDir: dir,
+ exportedTargets: map[core.BuildLabel]bool{},
}
+ var exporter exporterImpl
if noTrim {
- return // We have already exported the whole directory
+ exporter = newNoTrimExporter(base)
+ } else {
+ exporter = newTrimmedExporter(base)
}
- for pkg := range packages {
- if pkg.Name == parse.InternalPackageName {
- continue // This isn't a real package to be copied
- }
- if pkg.Subrepo != nil {
- continue // Don't copy subrepo BUILD files... they don't exist in our source tree
- }
- dest := filepath.Join(dir, pkg.Filename)
- if err := fs.CopyFile(pkg.Filename, dest, 0); err != nil {
- log.Fatalf("Failed to copy BUILD file %s: %s\n", pkg.Filename, err)
- }
- // Now rewrite the unused targets out of it
- var victims []string
- for _, target := range pkg.AllTargets() {
- if !e.exportedTargets[target.Label] && !target.HasParent() {
- victims = append(victims, target.Label.Name)
+ base.impl = exporter
+ return base
+}
+
+// baseExporter provides common fields and methods of other exporters.
+type baseExporter struct {
+ state *core.BuildState
+ targetDir string
+ targetCounter int
+
+ // exportedTargets maintains a record of the targets that have been exported so far.
+ exportedTargets map[core.BuildLabel]bool
+ // impl is a reference to the concrete exporter implementation. It's included for calling the
+ // specific exporter implementation from the common methods.
+ impl exporterImpl
+}
+
+// run specifies the main steps when running an export.
+func (be *baseExporter) run(targets core.BuildLabels) {
+ stop := make(chan struct{})
+ go be.startMonitor(stop)
+ defer close(stop)
+
+ be.exportRepoConfig()
+ be.impl.exportPreloaded()
+ be.exportTargets(targets)
+ be.impl.writePackageFiles()
+}
+
+func (be *baseExporter) startMonitor(stop chan struct{}) {
+ // this total is meant to provide some general idea of the progress but in reality we might
+ // visit more than the targets currently in the build graph (those will have to be parsed
+ // adhoc).
+ total := len(be.state.Graph.AllTargets())
+ startTime := time.Now()
+ isTerminal := cli.StdErrIsATerminal
+
+ if isTerminal {
+ cli.CurrentBackend.SetPassthrough(false, 1, false)
+ defer cli.CurrentBackend.SetPassthrough(true, 1, false)
+ }
+
+ ticker := time.NewTicker(100 * time.Millisecond)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-stop:
+ log.Infof("Exported %d targets (%.1fs)...", be.targetCounter, time.Since(startTime).Seconds())
+ return
+ case <-ticker.C:
+ done := be.targetCounter
+ elapsed := time.Since(startTime).Seconds()
+ stats := be.state.SystemStats()
+
+ if isTerminal {
+ logs := cli.CurrentBackend.FlushOutput()
+ if len(logs) > 0 {
+ // Remove footer line before writing logs.
+ cli.Fprintf(os.Stderr, "${RESETLN}")
+ for _, line := range logs {
+ fmt.Fprintln(os.Stderr, line)
+ }
+ }
+ cli.Fprintf(os.Stderr, "${RESETLN}${BOLD_WHITE}Exporting${RESET} [${BOLD_GREEN}%d${RESET}/${BOLD_GREEN}%d${RESET}, ${BOLD_YELLOW}%.1fs${RESET}]:: CPU: %5.1f%% Mem: %5.1f%% IO: %5.1f%%${RESET}",
+ done, total, elapsed, stats.CPU.Used, stats.Memory.UsedPercent, stats.CPU.IOWait)
}
}
- if err := gc.RewriteFile(state, dest, victims); err != nil {
- log.Fatalf("Failed to rewrite BUILD file: %s\n", err)
- }
}
}
-func (e *export) exportPlzConf() {
- profiles, err := filepath.Glob(".plzconfig*")
+// exportRepoConfig exports the repository's configuration files (e.g., .gitignore, .plzconfig and
+// its platform-specific variants) to the target export directory.
+func (be *baseExporter) exportRepoConfig() {
+ files, err := filepath.Glob(".plzconfig*")
if err != nil {
log.Fatalf("failed to glob .plzconfig files: %v", err)
}
- for _, file := range profiles {
- path := filepath.Join(e.targetDir, file)
- if err := os.RemoveAll(path); err != nil {
- log.Fatalf("failed to copy .plzconfig file %s: %v", file, err)
+ if info, err := os.Stat(".gitignore"); err == nil {
+ files = append(files, info.Name())
+ }
+
+ for _, file := range files {
+ targetPath := filepath.Join(be.targetDir, file)
+ if err := os.RemoveAll(targetPath); err != nil {
+ log.Fatalf("failed to remove file %s: %v", file, err)
}
- if err := fs.CopyFile(file, path, 0); err != nil {
- log.Fatalf("failed to copy .plzconfig file %s: %v", file, err)
+ if err := fs.CopyFile(file, targetPath, 0); err != nil {
+ log.Fatalf("failed to copy file %s: %v", file, err)
}
}
}
-// exportSources exports any source files (srcs and data) for the rule
-func (e *export) exportSources(target *core.BuildTarget) {
- for _, src := range append(target.AllSources(), target.AllData()...) {
- if _, ok := src.Label(); !ok { // We'll handle these dependencies later
- for _, p := range src.FullPaths(e.state.Graph) {
- if !filepath.IsAbs(p) { // Don't copy system file deps.
- if err := fs.RecursiveCopy(p, filepath.Join(e.targetDir, p), 0); err != nil {
- log.Fatalf("Error copying file: %s\n", err)
- }
- }
- }
+// exportTargets exports the set of targets identified by the given build labels.
+func (be *baseExporter) exportTargets(labels core.BuildLabels) {
+ for _, l := range labels {
+ if be.exportedTargets[l] {
+ continue
+ }
+ target, err := be.getTarget(l)
+ if err != nil {
+ log.Errorf("Unable to lookup target %s: %s", l, err)
+ continue
}
+ be.impl.exportTarget(target)
}
}
-var ignoreDirectories = map[string]bool{
- "plz-out": true,
- ".git": true,
- ".svn": true,
- ".hg": true,
+// exportDependencies exports dependencies of a target.
+func (be *baseExporter) exportDependencies(target *core.BuildTarget) {
+ deps := target.DeclaredDependencies()
+ be.exportTargets(deps)
}
-// exportPackage exports the package BUILD file containing the given target and all sources
-func (e *export) exportPackage(target *core.BuildTarget) {
- pkgName := target.Label.PackageName
- if pkgName == parse.InternalPackageName {
- return
- }
- if e.exportedPackages[pkgName] {
- return
- }
- e.exportedPackages[pkgName] = true
-
- pkgDir := filepath.Clean(pkgName)
-
- err := filepath.WalkDir(pkgDir, func(path string, d iofs.DirEntry, err error) error {
- if err != nil {
- return err
+// exportSources exports all files required by the target.
+func (be *baseExporter) exportSources(target *core.BuildTarget) {
+ for _, src := range target.AllBuildInputs() {
+ if _, ok := src.Label(); ok {
+ continue // These will be handled as dependencies later
}
- if d.IsDir() {
- if path != pkgDir && fs.IsPackage(e.state.Config.Parse.BuildFileName, path) {
- return filepath.SkipDir // We want to stop when we find another package in our dir tree
- }
- if ignoreDirectories[d.Name()] {
- return filepath.SkipDir
+ paths := src.Paths(be.state.Graph)
+ if target.Subrepo != nil { // Adjusting for local subrepos
+ for i, p := range paths {
+ paths[i] = target.Subrepo.Dir(p)
}
- return nil
}
- if !d.Type().IsRegular() {
- return nil // Ignore symlinks, which are almost certainly generated sources.
+ be.exportFiles(paths)
+ }
+}
+
+func (be *baseExporter) exportFiles(paths []string) {
+ for _, p := range paths {
+ if filepath.IsAbs(p) { // Don't copy system file deps.
+ log.Debugf("System dependency detected, skipping...: %s", p)
+ continue
}
- dest := filepath.Join(e.targetDir, path)
- if err := fs.EnsureDir(dest); err != nil {
- return err
+ dest := filepath.Join(be.targetDir, p)
+ if err := fs.RecursiveCopy(p, dest, 0); err != nil {
+ log.Warningf("Error copying file, skipping...: %s", err)
}
- return fs.CopyFile(path, dest, 0)
- })
- if err != nil {
- log.Fatalf("failed to export package %s for %s: %v", pkgName, target.Label, err)
}
}
-// export implements the logic of ToDir, but prevents repeating targets.
-func (e *export) export(target *core.BuildTarget) {
- if e.exportedTargets[target.Label] {
- return
- }
- // We want to export the package that made this subrepo available, but we still need to walk the target deps
- // as it may depend on other subrepos or first party targets
- if target.Subrepo != nil {
- e.export(target.Subrepo.Target)
- } else if e.noTrim {
- // Export the whole package, rather than trying to trim the package down to only the targets we need
- e.exportPackage(target)
- } else {
- e.exportSources(target)
+// getTarget attempts to lookup a target in the build graph.
+// This is a synchronous lookup and it assumes that [state.ForceParseEntirePackage]
+// was enabled during the parse phase to guarantee that all required targets are pre-parsed.
+func (be *baseExporter) getTarget(label core.BuildLabel) (*core.BuildTarget, error) {
+ target := be.state.Graph.Target(label)
+ if target == nil {
+ return nil, fmt.Errorf("target %s not found in graph", label)
}
+ return target, nil
+}
- e.exportedTargets[target.Label] = true
- for _, dep := range target.Dependencies() {
- e.export(dep)
- }
- for _, subinclude := range e.state.Graph.PackageOrDie(target.Label).AllSubincludes(e.state.Graph) {
- e.export(e.state.Graph.TargetOrDie(subinclude))
- }
- if parent := target.Parent(e.state.Graph); parent != nil && parent != target {
- e.export(parent)
+// getPackage attempts to lookup a package in the build graph.
+// This is a synchronous lookup and it assumes that [state.ForceParseEntirePackage]
+// was enabled during the parse phase to guarantee that all required packages are pre-parsed.
+func (be *baseExporter) getPackage(label core.BuildLabel) (*core.Package, error) {
+ pkg := be.state.Graph.PackageByLabel(label)
+ if pkg == nil {
+ return nil, fmt.Errorf("package %s not found in graph", label.PackageName)
}
+ return pkg, nil
}
-// Outputs exports the outputs of a target.
-func Outputs(state *core.BuildState, dir string, targets []core.BuildLabel) {
- for _, label := range targets {
- target := state.Graph.TargetOrDie(label)
- for _, out := range target.Outputs() {
- fullPath := filepath.Join(dir, out)
- outDir := filepath.Dir(fullPath)
- if err := os.MkdirAll(outDir, core.DirPermissions); err != nil {
- log.Fatalf("Failed to create export dir %s: %s", outDir, err)
- }
- if err := fs.RecursiveCopy(filepath.Join(target.OutDir(), out), fullPath, target.OutMode()|0200); err != nil {
- log.Fatalf("Failed to copy export file: %s", err)
- }
- }
+// checkAndSetVisited is a helper to ensure we only visit the same target once.
+// It returns true if this is the first time the target is being exported.
+func (be *baseExporter) checkAndSetVisited(target *core.BuildTarget) bool {
+ visited := be.exportedTargets[target.Label]
+ if visited {
+ return false
}
+ be.exportedTargets[target.Label] = true
+ be.targetCounter++
+ return true
}
diff --git a/src/export/export_test.go b/src/export/export_test.go
new file mode 100644
index 0000000000..b088e66669
--- /dev/null
+++ b/src/export/export_test.go
@@ -0,0 +1,157 @@
+package export
+
+import (
+ "os"
+ "slices"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/thought-machine/please/src/core"
+ "github.com/thought-machine/please/src/parse/asp"
+)
+
+func TestMinimalSubincludeStatement(t *testing.T) {
+ testCases := []struct {
+ name string
+ statementStr string
+ availableLabels []core.BuildLabel
+ requiredLabels []core.BuildLabel
+ out string
+ }{
+ {
+ name: "Successful no pruning subinclude",
+ statementStr: `subinclude("//build_defs:test")`,
+ availableLabels: core.ParseBuildLabels([]string{"//build_defs:test"}),
+ requiredLabels: core.ParseBuildLabels([]string{"//build_defs:test"}),
+ out: `subinclude("//build_defs:test")`,
+ },
+
+ {
+ name: "Single subinclude (not required)",
+ statementStr: `subinclude("//build_defs:other")`,
+ availableLabels: core.ParseBuildLabels([]string{"//build_defs:other"}),
+ requiredLabels: nil,
+ out: "",
+ },
+ {
+ name: "Multiple subincludes (sorted and filtered)",
+ statementStr: `subinclude("//build_defs:test", "//build_defs:abc", "//build_defs:other")`,
+ availableLabels: core.ParseBuildLabels([]string{"//build_defs:test", "//build_defs:abc", "//build_defs:other"}),
+ requiredLabels: core.ParseBuildLabels([]string{"//build_defs:test", "//build_defs:abc"}),
+ out: "subinclude(\n" +
+ " \"//build_defs:test\",\n" +
+ " \"//build_defs:abc\",\n" +
+ ")",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ p := asp.NewParserOnly()
+ statements, err := p.ParseData([]byte(tc.statementStr), "BUILD")
+ assert.NoError(t, err)
+ assert.Len(t, statements, 1)
+ stmt := statements[0]
+
+ e := newExporter(nil, "", false).impl.(*trimmedExporter)
+
+ pkg := &core.Package{Name: "test"}
+ e.requiredSubincludes[pkg.Label()] = tc.requiredLabels
+ trimmer := trimmer{
+ pkg: pkg,
+ exporter: e,
+ origin: []byte(tc.statementStr),
+ }
+
+ assert.Equal(t, tc.out, trimmer.minimalSubincludeStatement(stmt, tc.availableLabels))
+ })
+ }
+}
+
+func TestFilterPackageFile(t *testing.T) {
+ testCases := []struct {
+ name string
+ required []string
+ expected string
+ }{
+ {
+ name: "Keep only A",
+ required: []string{"a"},
+ expected: "src/export/test_data/filter_expected_a.build",
+ },
+ {
+ name: "Keep only B",
+ required: []string{"b"},
+ expected: "src/export/test_data/filter_expected_b.build",
+ },
+ {
+ name: "Keep both",
+ required: []string{"a", "b"},
+ expected: "src/export/test_data/filter.build",
+ },
+ {
+ name: "Keep none",
+ required: []string{},
+ expected: "src/export/test_data/filter_expected_none.build",
+ },
+ }
+
+ contentPath := "src/export/test_data/filter.build"
+
+ p := asp.NewParserOnly()
+ statements, err := p.ParseFileOnly(contentPath)
+ assert.NoError(t, err)
+
+ pkg := core.NewPackage("test", core.WithPackageMetadata())
+ pkg.Filename = contentPath
+ targetLabels := walkASTRegisterTargets(t, statements, pkg, nil)
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ e := newExporter(nil, "", false).impl.(*trimmedExporter)
+ for _, name := range tc.required {
+ e.exportedTargets[targetLabels[name]] = true
+ }
+ e.visitedPackages[pkg.Label()] = true
+
+ p := asp.NewParserOnly()
+ got, err := e.trimPackage(p, pkg)
+ got = trimNewlines(got)
+ assert.NoError(t, err)
+
+ expected, err := os.ReadFile(tc.expected)
+ assert.NoError(t, err)
+ assert.Equal(t, string(expected), string(got))
+ })
+ }
+}
+
+// walkASTRegisterTargets is a test helper to register simple targets and their build statements.
+func walkASTRegisterTargets(t *testing.T, stmts []*asp.Statement, pkg *core.Package, toRegister []string) map[string]core.BuildLabel {
+ t.Helper()
+ targetLabels := map[string]core.BuildLabel{}
+ asp.WalkAST(stmts, func(stmt *asp.Statement) bool {
+ arg := asp.FindArgument(stmt, "name")
+ if arg == nil {
+ return true // Continue
+ }
+
+ // Not in targets we want to register, continue. Empty selection
+ // will cause all targets to be registered.
+ name := strings.Trim(arg.Value.Val.String, "\"")
+ if toRegister != nil && !slices.Contains(toRegister, name) {
+ return true
+ }
+
+ label := core.NewBuildLabel(pkg.Name, name)
+ targetLabels[name] = label
+ target := &core.BuildTarget{Label: label}
+ pkg.Metadata.RegisterTargetStatement(target.Label, func() core.BuildStatement {
+ return asp.NewBuildStatement(stmt)
+ })
+ return true
+ })
+ return targetLabels
+}
diff --git a/src/export/notrim_exporter.go b/src/export/notrim_exporter.go
new file mode 100644
index 0000000000..31d748ec4b
--- /dev/null
+++ b/src/export/notrim_exporter.go
@@ -0,0 +1,109 @@
+package export
+
+import (
+ "path/filepath"
+
+ "github.com/thought-machine/please/src/core"
+ "github.com/thought-machine/please/src/fs"
+ "github.com/thought-machine/please/src/parse"
+)
+
+// noTrimExporter implements an exporter that avoids trimming any packages by exporting all targets
+// and statements in a package.
+type noTrimExporter struct {
+ *baseExporter
+ // exportedPackages tracks which packages have already had their BUILD files exported.
+ exportedPackages map[core.BuildLabel]bool
+}
+
+func newNoTrimExporter(base *baseExporter) exporterImpl {
+ return &noTrimExporter{
+ baseExporter: base,
+ exportedPackages: map[core.BuildLabel]bool{},
+ }
+}
+
+// exportPreloaded implements [exporterImpl].
+func (nte *noTrimExporter) exportPreloaded() {
+ // Write any preloaded build defs
+ for _, preload := range nte.state.Config.Parse.PreloadBuildDefs {
+ if err := fs.RecursiveCopy(preload, filepath.Join(nte.targetDir, preload), 0); err != nil {
+ log.Errorf("Failed to copy preloaded build def %s: %s", preload, err)
+ }
+ }
+
+ for _, target := range nte.state.Config.Parse.PreloadSubincludes {
+ targets := append(nte.state.Graph.TransitiveSubincludes(target), target)
+ nte.exportTargets(targets)
+ }
+}
+
+// exportTarget implements [exporterImpl].
+func (nte *noTrimExporter) exportTarget(target *core.BuildTarget) {
+ pkg, err := nte.getPackage(target.Label)
+ if err != nil {
+ log.Errorf("Unable to lookup package %s: %s", target.Label, err)
+ return
+ }
+
+ if !nte.checkAndSetVisited(target) {
+ return
+ }
+
+ // We want to export the package that made this subrepo available, but we still need to walk the target deps
+ // as it may depend on other subrepos or first party targets
+ if target.Subrepo != nil {
+ nte.exportTarget(target.Subrepo.Target)
+ nte.exportDependencies(target)
+ return
+ }
+
+ nte.exportSubincludes(pkg)
+ nte.exportPackage(pkg)
+ nte.exportSources(target)
+ nte.exportDependencies(target)
+}
+
+// writePackageFiles implements [exporterImpl].
+func (nte *noTrimExporter) writePackageFiles() {
+ for pkgLabel := range nte.exportedPackages {
+ pkg, err := nte.getPackage(pkgLabel)
+ if err != nil {
+ log.Errorf("Unable to lookup package %s: %s", pkgLabel, err)
+ continue
+ }
+
+ exportedFilename := filepath.Join(nte.targetDir, pkg.Filename)
+ if pkg.Subrepo != nil {
+ exportedFilename = filepath.Join(nte.targetDir, pkg.Subrepo.Dir(pkg.Filename))
+ }
+ if err := fs.CopyFile(pkg.Filename, exportedFilename, 0); err != nil {
+ log.Errorf("failed to export package %s: %v", pkg.Name, err)
+ }
+ }
+}
+
+// exportPackage exports the package BUILD file.
+func (nte *noTrimExporter) exportPackage(pkg *core.Package) {
+ // Skip subrepos and internal packages. These will be generated by build statements in the exported
+ // repo or included in please internally.
+ if pkg.Subrepo != nil || pkg.Name == parse.InternalPackageName {
+ return
+ }
+
+ if nte.exportedPackages[pkg.Label()] {
+ return
+ }
+ nte.exportedPackages[pkg.Label()] = true
+
+ // Export all the targets in the provided package.
+ for _, target := range pkg.AllTargets() {
+ nte.exportTarget(target)
+ }
+}
+
+// exportSubincludes exports the subincluded targets.
+func (nte *noTrimExporter) exportSubincludes(pkg *core.Package) {
+ subincludes := pkg.AllSubincludes(nte.state.Graph)
+ nte.exportTargets(subincludes)
+}
diff --git a/src/export/test_data/filter.build b/src/export/test_data/filter.build
new file mode 100644
index 0000000000..35f90c2265
--- /dev/null
+++ b/src/export/test_data/filter.build
@@ -0,0 +1,17 @@
+# This is a header comment
+package(default_visibility = ["PUBLIC"])
+
+VERSION = "1.2.3"
+
+target(
+ name = "a",
+ # This is a comment inside a target
+ srcs = ["a.py"],
+ version = VERSION,
+)
+
+# This is a middle comment
+target(
+ name = "b",
+ srcs = ["b.py"],
+)
diff --git a/src/export/test_data/filter_expected_a.build b/src/export/test_data/filter_expected_a.build
new file mode 100644
index 0000000000..97f1b7d937
--- /dev/null
+++ b/src/export/test_data/filter_expected_a.build
@@ -0,0 +1,13 @@
+# This is a header comment
+package(default_visibility = ["PUBLIC"])
+
+VERSION = "1.2.3"
+
+target(
+ name = "a",
+ # This is a comment inside a target
+ srcs = ["a.py"],
+ version = VERSION,
+)
+
+# This is a middle comment
diff --git a/src/export/test_data/filter_expected_b.build b/src/export/test_data/filter_expected_b.build
new file mode 100644
index 0000000000..ec40334a9b
--- /dev/null
+++ b/src/export/test_data/filter_expected_b.build
@@ -0,0 +1,10 @@
+# This is a header comment
+package(default_visibility = ["PUBLIC"])
+
+VERSION = "1.2.3"
+
+# This is a middle comment
+target(
+ name = "b",
+ srcs = ["b.py"],
+)
diff --git a/src/export/test_data/filter_expected_none.build b/src/export/test_data/filter_expected_none.build
new file mode 100644
index 0000000000..074deca2e0
--- /dev/null
+++ b/src/export/test_data/filter_expected_none.build
@@ -0,0 +1,6 @@
+# This is a header comment
+package(default_visibility = ["PUBLIC"])
+
+VERSION = "1.2.3"
+
+# This is a middle comment
diff --git a/src/export/trimmed_exporter.go b/src/export/trimmed_exporter.go
new file mode 100644
index 0000000000..d5812d1678
--- /dev/null
+++ b/src/export/trimmed_exporter.go
@@ -0,0 +1,240 @@
+package export
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "slices"
+
+ "github.com/thought-machine/please/src/core"
+ "github.com/thought-machine/please/src/fs"
+ "github.com/thought-machine/please/src/parse"
+ "github.com/thought-machine/please/src/parse/asp"
+)
+
+// trimmedExporter implements an exporter that trims packages to reach a minimal exported repo.
+type trimmedExporter struct {
+ *baseExporter
+ // visitedPackages maintains a record of the packages visited during the export process.
+ visitedPackages map[core.BuildLabel]bool
+ // requiredSubincludes maps packages to the subinclude labels they require.
+ requiredSubincludes map[core.BuildLabel]core.BuildLabels
+ // preloadedSubincludes tracks subincludes that are preloaded and don't need explicit export.
+ preloadedSubincludes map[core.BuildLabel]bool
+}
+
+func newTrimmedExporter(base *baseExporter) exporterImpl {
+ return &trimmedExporter{
+ baseExporter: base,
+ visitedPackages: map[core.BuildLabel]bool{},
+ requiredSubincludes: map[core.BuildLabel]core.BuildLabels{},
+ preloadedSubincludes: map[core.BuildLabel]bool{},
+ }
+}
+
+// exportPreloaded implements [exporterImpl].
+func (e *trimmedExporter) exportPreloaded() {
+ // Write any preloaded build defs
+ for _, preload := range e.state.Config.Parse.PreloadBuildDefs {
+ if err := fs.RecursiveCopy(preload, filepath.Join(e.targetDir, preload), 0); err != nil {
+ log.Fatalf("Failed to copy preloaded build def %s: %s", preload, err)
+ }
+ }
+
+ for _, target := range e.state.GetPreloadedSubincludes() {
+ targets := append(e.state.Graph.TransitiveSubincludes(target), target)
+ for _, t := range targets {
+ e.preloadedSubincludes[t] = true
+ }
+ e.exportTargets(targets)
+ }
+}
+
+// exportTarget implements [exporterImpl].
+func (e *trimmedExporter) exportTarget(target *core.BuildTarget) {
+ if !e.checkAndSetVisited(target) {
+ return
+ }
+
+ // Skip export for internal packages
+ if target.Label.PackageName == parse.InternalPackageName {
+ return
+ }
+ // We want to export the package that made this subrepo available, but we still need to walk the
+ // target deps as it may depend on other subrepos or first party targets
+ if target.Subrepo.IsExternal() {
+ e.exportTarget(target.Subrepo.Target)
+ e.exportDependencies(target)
+ return
+ }
+
+ e.exportSources(target)
+ e.exportDependencies(target)
+
+ pkg, err := e.getPackage(target.Label)
+ if err != nil {
+ log.Errorf("Unable to lookup package %s: %s", target.Label, err)
+ return
+ }
+ e.exportSubincludes(pkg, target.Label)
+ e.exportRelatedTargets(pkg, target.Label)
+
+ if !e.visitedPackages[pkg.Label()] {
+ // Export subincluded targets required for other package statements, e.g. variable
+ // declaration, during the first visit of a package.
+ e.exportPackageRequirements(pkg)
+ e.visitedPackages[pkg.Label()] = true
+ }
+}
+
+// writePackageFiles implements [exporterImpl].
+func (e *trimmedExporter) writePackageFiles() {
+ p := asp.NewParserOnly()
+ for pkgLabel := range e.visitedPackages {
+ pkg := e.state.Graph.PackageOrDie(pkgLabel)
+ filteredBytes, err := e.trimPackage(p, pkg)
+ if err != nil {
+ log.Errorf("Failed to filter the build statements of package %s: %v", pkg.Label(), err)
+ continue
+ }
+
+ e.writeExportedPackageFile(pkg, trimNewlines(filteredBytes))
+ }
+}
+
+// exportSubincludes exports the subincluded targets required to generate the target and selects them to
+// later be written to the package as statements.
+func (e *trimmedExporter) exportSubincludes(pkg *core.Package, target core.BuildLabel) {
+ // Get the actively used subincludes of the target and propagate all transitive subincludes required
+ // by our used subinclude targets. FindRequiredSubincludes will report the required subincludes
+ // for this target at the package level but we need to propagate the subincluded targets inside
+ // build definitions since we are not trimming build_defs files.
+ usedSubincludes, err := pkg.Metadata.FindRequiredSubincludes(target)
+ if err != nil {
+ log.Fatalf("failed to find required subincludes for target %s: %s", target, err)
+ }
+ e.setPackageSubincludes(pkg, usedSubincludes)
+
+ allSubincludes := usedSubincludes
+ for _, sub := range usedSubincludes {
+ for _, trans := range e.state.Graph.TransitiveSubincludes(sub) {
+ if !slices.Contains(allSubincludes, trans) {
+ allSubincludes = append(allSubincludes, trans)
+ }
+ }
+ }
+
+ e.exportTargets(allSubincludes)
+}
+
+// exportPackageRequirements exports any extra package requirements, for example the subincluded
+// targets and files that are required by the package but are not linked to any [core.BuildTarget].
+func (e *trimmedExporter) exportPackageRequirements(pkg *core.Package) {
+ subincludes, files := pkg.Metadata.FindPackageLevelRequirements()
+ e.setPackageSubincludes(pkg, subincludes)
+ e.exportTargets(subincludes)
+ e.exportFiles(files)
+}
+
+// setPackageSubincludes marks the package-level required subincludes after the export. This will be
+// used for trimming subinclude statements with [trimmer].
+func (e *trimmedExporter) setPackageSubincludes(pkg *core.Package, subincludes core.BuildLabels) {
+ for _, subinclude := range subincludes {
+ // skip for preloaded subincludes, these are handled separately at the start to ensure they are
+ // exported even if not directly used by an exported target.
+ if e.preloadedSubincludes[subinclude] {
+ continue
+ }
+
+ pkgLabel := pkg.Label()
+ required := e.requiredSubincludes[pkgLabel]
+ if !slices.Contains(required, subinclude) {
+ required = append(required, subinclude)
+ }
+ e.requiredSubincludes[pkgLabel] = required
+ }
+}
+
+// exportRelatedTargets looks up and exports all build targets that were declared within the same
+// build statement (e.g., adjacent targets in build def) as the specified target. This ensures that
+// all co-defined targets are preserved in the exported BUILD file, preventing unresolved references
+// or partial declarations.
+func (e *trimmedExporter) exportRelatedTargets(pkg *core.Package, target core.BuildLabel) {
+ relatedTargets, err := pkg.Metadata.FindRelatedTargets(target)
+ if err != nil {
+ log.Fatalf("failed to find related targets for %s: %s", target, err)
+ }
+ e.exportTargets(relatedTargets)
+}
+
+// WriteExportedPackageFile creates a new package (BUILD) file in the exported dir and writes to it.
+func (e *trimmedExporter) writeExportedPackageFile(pkg *core.Package, content []byte) {
+ filename := pkg.Filename
+ exportedFilename := filepath.Join(e.targetDir, filename)
+ if pkg.Subrepo != nil { // Adjusting for local subrepos
+ exportedFilename = filepath.Join(e.targetDir, pkg.Subrepo.Dir(filename))
+ }
+ f, err := fs.OpenDirFile(exportedFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0664)
+ if err != nil {
+ log.Fatalf("Failed to create and open exported BUILD file for %s: %v", exportedFilename, err)
+ }
+ defer f.Close()
+
+ if _, err := f.Write(content); err != nil {
+ log.Errorf("Failed to write to exported BUILD file %s: %v", f.Name(), err)
+ }
+}
+
+// trimPackage filters the statements to be written to the exported BUILD file.
+func (e *trimmedExporter) trimPackage(p *asp.Parser, pkg *core.Package) ([]byte, error) {
+ filename := pkg.Filename
+ if pkg.Subrepo != nil { // Adjusting for local subrepos
+ filename = pkg.Subrepo.Dir(filename)
+ }
+
+ parsed, err := p.ParseFileOnly(filename)
+ if err != nil {
+ return nil, fmt.Errorf("Parsing original BUILD file: %v", err)
+ }
+
+ content, err := os.ReadFile(filename)
+ if err != nil {
+ return nil, fmt.Errorf("Opening original BUILD file: %v", err)
+ }
+
+ trimmer := trimmer{
+ origin: content,
+ pkg: pkg,
+ exporter: e,
+ // assuming max len of the original file to avoid reallocations.
+ bytes: make([]byte, 0, len(content)),
+ }
+ trimmer.trimBlock(parsed, 0, asp.Position(len(content)))
+
+ return trimmer.bytes, nil
+}
+
+// trimNewlines trims leading and trailing whitespace and collapses 3+ consecutive newlines to 2.
+func trimNewlines(b []byte) []byte {
+ trimmed := bytes.TrimSpace(b)
+ var pointer, newlines int
+ for _, val := range trimmed {
+ if val == '\n' {
+ newlines++
+ if newlines > 2 {
+ continue // Skip third (or more) consecutive newline
+ }
+ } else {
+ newlines = 0
+ }
+ trimmed[pointer] = val
+ pointer++
+ }
+ trimmed = trimmed[:pointer]
+
+ if len(trimmed) > 0 {
+ trimmed = append(trimmed, '\n') // Trailing newline
+ }
+ return trimmed
+}
diff --git a/src/export/trimmer.go b/src/export/trimmer.go
new file mode 100644
index 0000000000..1890748c4f
--- /dev/null
+++ b/src/export/trimmer.go
@@ -0,0 +1,225 @@
+package export
+
+import (
+ "slices"
+
+ "github.com/please-build/buildtools/build"
+
+ "github.com/thought-machine/please/src/core"
+ "github.com/thought-machine/please/src/parse/asp"
+)
+
+var passExpression = []byte("pass # Trimmed during export")
+
+// trimmer implements the filtering logic for statements in package files.
+type trimmer struct {
+ // origin are the bytes from the original package file.
+ origin []byte
+ // pkg references the package being trimmed.
+ pkg *core.Package
+ // bytes contain the content to be written after the trimming process.
+ bytes []byte
+ // exporter is used to lookup target related data from the export process, e.g. which targets are
+ // required.
+ exporter *trimmedExporter
+}
+
+// statementConsumer defines the type for methods used to visit each statement during a
+// file walk. The method accepts the currently interpreted statement.
+type statementConsumer func(*asp.Statement)
+
+// walkFile will walk the file from the specified start to end, consuming the statements and optionally writing non-statement bytes found along the way (e.g. comments and blank space).
+func (t *trimmer) walkFile(stmts []*asp.Statement, start, end asp.Position, consumer statementConsumer) {
+ // cursor tracks the position in a block that's being interpreted.
+ cursor := start
+ for _, stmt := range stmts {
+ // Write content that's between stmts (e.g. comments). We skip these while parsing so it won't
+ // be included in "parsedStmts" but we want the resulting BUILD file to include these.
+ if cursor < stmt.Pos {
+ t.copy(cursor, stmt.Pos)
+ }
+
+ // Consume stmt with specified method
+ consumer(stmt)
+
+ // Move the cursor to the end of the processed statement. The cursor will enable writing of lines
+ // that are not considered statements by the parser (e.g. comments, new lines).
+ cursor = stmt.EndPos
+ }
+
+ // Write the rest of the original file (non build targets)
+ t.copy(cursor, end)
+}
+
+// trimBlock visits all the statements in a block and trims undesired statements.
+func (t *trimmer) trimBlock(stmts []*asp.Statement, blockStart, blockEnd asp.Position) bool {
+ var written bool
+ t.walkFile(stmts, blockStart, blockEnd, func(stmt *asp.Statement) {
+ if stmt.If != nil {
+ if t.trimIf(stmt) {
+ written = true
+ }
+ } else if stmt.For != nil {
+ if t.trimFor(stmt) {
+ written = true
+ }
+ } else if stmt.Ident != nil && stmt.Ident.Name == "subinclude" {
+ t.trimSubinclude(stmt)
+ written = true
+ } else if targets := t.statementTargets(stmt); len(targets) > 0 {
+ // Meaning it is a build statement that creates build targets.
+ if t.anyExported(targets) {
+ t.copy(stmt.Pos, stmt.EndPos)
+ written = true
+ }
+ } else {
+ // Write every other statement.
+ // If the statement didn't generate any targets (e.g. variable assignments, package() calls),
+ // we keep it to ensure the BUILD file remains valid.
+ t.copy(stmt.Pos, stmt.EndPos)
+ written = true
+ }
+ })
+ if !written {
+ t.write(passExpression)
+ }
+ return written
+}
+
+// trimIf will trim an if-else statement by exporting only the required targets, but keeping the
+// if-else primitive -- a implementation decision to help understand the changes caused by an export
+// when using a source-control management (SCM) system.
+func (t *trimmer) trimIf(stmt *asp.Statement) bool {
+ type clause struct {
+ hStart, hEnd asp.Position
+ stmts []*asp.Statement
+ }
+
+ clauses := []clause{
+ {hStart: stmt.If.HeaderPos, hEnd: stmt.If.HeaderEndPos, stmts: stmt.If.Statements},
+ }
+ for _, elif := range stmt.If.Elif {
+ clauses = append(clauses,
+ clause{hStart: elif.HeaderPos, hEnd: elif.HeaderEndPos, stmts: elif.Statements})
+ }
+ if len(stmt.If.ElseStatements) > 0 {
+ clauses = append(clauses,
+ clause{hStart: stmt.If.ElseHeaderPos, hEnd: stmt.If.ElseHeaderEndPos, stmts: stmt.If.ElseStatements})
+ }
+
+ // In an if-else statement only the interpreted/evaluated block will generate targets, meaning
+ // that normally only one of the clauses is interpreted, however an if statement could be inside of
+ // a loop where the clause condition depends on the iteration meaning more than one clause
+ // could end up being interpreted. Because of that we'll visit all the clauses, writing only the
+ // required statements -- any clauses with interpreted statements should be visited.
+ var requiredClauses = make([]bool, len(clauses))
+ for i, c := range clauses {
+ if len(c.stmts) == 0 {
+ continue
+ }
+ bStmt := asp.NewBuildStatement(c.stmts[0])
+ required := t.pkg.Metadata.IsInterpretedStatement(bStmt)
+ requiredClauses[i] = required
+ }
+
+ for i, c := range clauses {
+ // Write clause header
+ t.copy(c.hStart, c.hEnd)
+
+ // Visit statements in block
+ end := stmt.EndPos
+ if i+1 < len(clauses) {
+ end = clauses[i+1].hStart
+ }
+ if requiredClauses[i] {
+ t.trimBlock(c.stmts, c.hEnd, end)
+ } else {
+ t.passBlock(c.stmts, c.hEnd, end)
+ }
+ }
+ return true
+}
+
+func (t *trimmer) trimFor(stmt *asp.Statement) bool {
+ if len(stmt.For.Statements) == 0 {
+ return false
+ }
+
+ hStart, hEnd := stmt.Pos, stmt.For.Statements[0].Pos
+ t.copy(hStart, hEnd)
+
+ t.trimBlock(stmt.For.Statements, hEnd, stmt.EndPos)
+ return true
+}
+
+func (t *trimmer) trimSubinclude(stmt *asp.Statement) {
+ bStmt := asp.NewBuildStatement(stmt)
+ stmtLabels := t.pkg.Metadata.GetSubincludedLabels(bStmt)
+ subStmt := t.minimalSubincludeStatement(stmt, stmtLabels)
+ t.write([]byte(subStmt))
+}
+
+// passBlock skips all ASP statements (keeping comments and blank space) and writes a single "pass"
+// primitive.
+func (t *trimmer) passBlock(stmts []*asp.Statement, blockStart, blockEnd asp.Position) {
+ var passWritten bool
+ t.walkFile(stmts, blockStart, blockEnd, func(s *asp.Statement) {
+ // When the trim results in an empty block, i.e. no statement are written, we write
+ // the "pass" primitive. This is useful when parsing inner blocks (e.g. if-else stmts).
+ if !passWritten {
+ passWritten = true
+ t.write(passExpression)
+ }
+ })
+}
+
+func (t *trimmer) statementTargets(stmt *asp.Statement) core.BuildLabels {
+ bStmt := asp.NewBuildStatement(stmt)
+ return t.pkg.Metadata.FindTargets(bStmt)
+}
+
+func (t *trimmer) anyExported(labels core.BuildLabels) bool {
+ required := slices.ContainsFunc(labels, func(l core.BuildLabel) bool {
+ return t.exporter.exportedTargets[l]
+ })
+ return required
+}
+
+// minimalSubincludeStatement generates a subinclude statement containing only the required labels.
+func (t *trimmer) minimalSubincludeStatement(stmt *asp.Statement, available core.BuildLabels) string {
+ var filteredLabels core.BuildLabels
+ required := t.exporter.requiredSubincludes[t.pkg.Label()]
+ for _, label := range available {
+ if slices.Contains(required, label) {
+ filteredLabels = append(filteredLabels, label)
+ }
+ }
+
+ if len(filteredLabels) == 0 {
+ return ""
+ }
+ if len(available) == len(filteredLabels) {
+ // If all labels are required just keep the statement as is.
+ return string(t.origin[stmt.Pos:stmt.EndPos])
+ }
+
+ call := &build.CallExpr{
+ X: &build.Ident{Name: "subinclude"},
+ }
+ for _, label := range filteredLabels {
+ call.List = append(call.List, &build.StringExpr{Value: label.ShortString(t.pkg.Label())})
+ }
+
+ return build.FormatString(call)
+}
+
+func (t *trimmer) copy(start, end asp.Position) {
+ if start < 0 || start > end || int(end) > len(t.origin) {
+ return
+ }
+ t.bytes = append(t.bytes, t.origin[start:end]...)
+}
+
+func (t *trimmer) write(bytes []byte) {
+ t.bytes = append(t.bytes, bytes...)
+}
diff --git a/src/gc/gc.go b/src/gc/gc.go
index 1bdc7463df..95eccfd475 100644
--- a/src/gc/gc.go
+++ b/src/gc/gc.go
@@ -173,7 +173,7 @@ func addTarget(graph *core.BuildGraph, m targetMap, target *core.BuildTarget) {
for _, dep := range target.Dependencies() {
addTarget(graph, m, dep)
}
- if target.Subrepo != nil && target.Subrepo.Target != nil {
+ if target.Subrepo.IsExternal() {
addTarget(graph, m, target.Subrepo.Target)
}
}
@@ -208,7 +208,7 @@ func publicDependencies(graph *core.BuildGraph, target *core.BuildTarget) []*cor
}
}
}
- if target.Subrepo != nil && target.Subrepo.Target != nil {
+ if target.Subrepo.IsExternal() {
ret = append(ret, target.Subrepo.Target)
}
return ret
diff --git a/src/output/interactive_display.go b/src/output/interactive_display.go
index 1940c9e590..9224ac44dd 100644
--- a/src/output/interactive_display.go
+++ b/src/output/interactive_display.go
@@ -100,9 +100,14 @@ func (d *interactiveDisplay) Update(targets []buildingTarget) {
d.maxRows, d.maxCols = cli.CurrentBackend.MaxDimensions()
d.moveToFirstLine()
d.printLines(targets)
- for _, line := range cli.CurrentBackend.Output() {
- d.printf("${ERASE_AFTER}%s\n", line)
+ logs := cli.CurrentBackend.Output()
+ if len(logs) > 0 {
+ d.printf("${ERASE_AFTER}Messages:\n")
d.lines++
+ for _, line := range logs {
+ d.printf("${ERASE_AFTER}%s\n", line)
+ d.lines++
+ }
}
// Clean out any lines that were visible last time but are not now.
if d.lines < d.lastLines {
diff --git a/src/parse/asp/build_statement.go b/src/parse/asp/build_statement.go
new file mode 100644
index 0000000000..13a37ae004
--- /dev/null
+++ b/src/parse/asp/build_statement.go
@@ -0,0 +1,26 @@
+package asp
+
+import "github.com/thought-machine/please/src/core"
+
+// buildStatement implements [core.BuildStatement] to track the byte offsets
+// of a parsed statement within a BUILD file.
+type buildStatement struct {
+ start, end int
+}
+
+// StartPos implements [core.BuildStatement].
+func (b buildStatement) StartPos() int { return b.start }
+
+// EndPos implements [core.BuildStatement].
+func (b buildStatement) EndPos() int { return b.end }
+
+// NewBuildStatement creates a new [core.BuildStatement] from an [asp.Statement].
+func NewBuildStatement(stmt *Statement) core.BuildStatement {
+ if stmt == nil {
+ return nil
+ }
+ return buildStatement{
+ start: int(stmt.Pos),
+ end: int(stmt.EndPos),
+ }
+}
diff --git a/src/parse/asp/builtins.go b/src/parse/asp/builtins.go
index 8be2cbc8e4..acfbba67cb 100644
--- a/src/parse/asp/builtins.go
+++ b/src/parse/asp/builtins.go
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
+ "iter"
"path/filepath"
"reflect"
"regexp"
@@ -210,6 +211,8 @@ func buildRule(s *scope, args []pyObject) pyObject {
s.Assert(s.pkg.Target(target.Label.Name) == nil, "Duplicate build target in %s: %s", s.pkg.Name, target.Label.Name)
populateTarget(s, target, args)
s.state.AddTarget(s.pkg, target)
+ s.pkg.Metadata.RegisterTargetStatement(target.Label, s.CurrentBuildStatement())
+
if s.Callback {
target.AddedPostBuild = true
}
@@ -307,7 +310,7 @@ func bazelLoad(s *scope, args []pyObject) pyObject {
}
filename = subrepo.Dir(filename)
}
- s.SetAll(s.interpreter.Subinclude(s, filename, l, false), false)
+ s.SetAllWithOrigin(s.interpreter.Subinclude(s, filename, l, false), false, &l)
return None
}
@@ -331,23 +334,12 @@ func subinclude(s *scope, args []pyObject) pyObject {
if s.contextPackage() == nil {
s.Error("cannot subinclude from this scope")
}
- var si []string
- for _, arg := range args {
- if l, ok := arg.(pyList); ok {
- for _, e := range l {
- if l, ok := e.(pyString); ok {
- si = append(si, string(l))
- } else {
- s.Error("cannot subinclude type %s", e.Type())
- }
- }
- } else if l, ok := arg.(pyString); ok {
- si = append(si, string(l))
- } else {
- s.Error("cannot subinclude type %s", arg.Type())
+
+ var labels = make(core.BuildLabels, 0, len(args))
+ for arg, err := range subincludeArgs(args) {
+ if err != nil {
+ s.Error("%s", err.Error())
}
- }
- for _, arg := range si {
label, annotation := core.SplitLabelAnnotation(arg)
t := subincludeTarget(s, s.parseLabelInContextPkg(label))
s.Assert(s.contextPackage().Label().CanSee(s.state, t), "Target %s isn't visible to be subincluded into %s", t.Label, s.contextPackage().Label())
@@ -366,12 +358,47 @@ func subinclude(s *scope, args []pyObject) pyObject {
outs = t.Outputs()
}
for _, out := range outs {
- s.SetAll(s.interpreter.Subinclude(s, filepath.Join(t.OutDir(), out), t.Label, false), false)
+ s.SetAllWithOrigin(s.interpreter.Subinclude(s, filepath.Join(t.OutDir(), out), t.Label, false), false, &t.Label)
}
+ labels = append(labels, t.Label)
+ }
+ s.metadataRegisterSubincludes(labels)
+ if s.pkg != nil {
+ s.pkg.Metadata.RegisterSubincludeStatement(s.CurrentBuildStatement())
}
return None
}
+// subincludeArgs extracts and validates the arguments from a subinclude() call. Acceptable values
+// are strings and lists of strings.
+func subincludeArgs(args []pyObject) iter.Seq2[string, error] {
+ return func(yield func(string, error) bool) {
+ for _, arg := range args {
+ if l, ok := arg.(pyList); ok {
+ for _, e := range l {
+ if s, ok := e.(pyString); ok {
+ if !yield(string(s), nil) {
+ return
+ }
+ } else {
+ if !yield("", fmt.Errorf("cannot subinclude type %s", e.Type())) {
+ return
+ }
+ }
+ }
+ } else if s, ok := arg.(pyString); ok {
+ if !yield(string(s), nil) {
+ return
+ }
+ } else {
+ if !yield("", fmt.Errorf("cannot subinclude type %s", arg.Type())) {
+ return
+ }
+ }
+ }
+ }
+}
+
// subincludeTarget returns the target for a subinclude() call to a label.
// It blocks until the target exists and is built.
func subincludeTarget(s *scope, l core.BuildLabel) *core.BuildTarget {
@@ -729,6 +756,7 @@ func glob(s *scope, args []pyObject) pyObject {
exclude = exclude[:len(exclude)-len(s.state.Config.Parse.BuildFileName)]
log.Fatalf("glob(include=%s, exclude=%s) in %s returned no files. If this is intended, set allow_empty=True on the glob.", include, exclude, s.pkg.Filename)
}
+ s.metadataRegisterFiles(s.pkg.Name, glob)
return fromStringList(glob)
}
diff --git a/src/parse/asp/builtins_test.go b/src/parse/asp/builtins_test.go
index 02e622b0c2..a3a6079280 100644
--- a/src/parse/asp/builtins_test.go
+++ b/src/parse/asp/builtins_test.go
@@ -127,3 +127,34 @@ func TestObjLen(t *testing.T) {
assert.EqualValues(t, 1, objLen(d))
assert.EqualValues(t, 1, objLen(d.Freeze()))
}
+
+func TestSubincludeArgs(t *testing.T) {
+ // Test basic parsing of mixed strings and lists of strings.
+ args := []pyObject{
+ pyString("//src/core:all"),
+ pyList{
+ pyString("//src/parse:all"), pyString("//src/build:all"),
+ pyInt(123), // An integer inside a list should cause an error
+ },
+ }
+
+ type result struct {
+ val string
+ err error
+ }
+ var results []result
+ for val, err := range subincludeArgs(args) {
+ results = append(results, result{val: val, err: err})
+ }
+
+ assert.Len(t, results, 4)
+ assert.Equal(t, "//src/core:all", results[0].val)
+ assert.Nil(t, results[0].err)
+ assert.Equal(t, "//src/parse:all", results[1].val)
+ assert.Nil(t, results[1].err)
+ assert.Equal(t, "//src/build:all", results[2].val)
+ assert.Nil(t, results[2].err)
+ // Test for an error when yielding the integer.
+ assert.Equal(t, "", results[3].val)
+ assert.Error(t, results[3].err)
+}
diff --git a/src/parse/asp/grammar.go b/src/parse/asp/grammar.go
index 18084cabc0..1181b71deb 100644
--- a/src/parse/asp/grammar.go
+++ b/src/parse/asp/grammar.go
@@ -70,16 +70,22 @@ type ForStatement struct {
// An IfStatement implements the if-elif-else statement.
type IfStatement struct {
- Condition Expression
- Statements []*Statement
- Elif []IfStatementElif
- ElseStatements []*Statement
+ HeaderPos Position
+ HeaderEndPos Position
+ Condition Expression
+ Statements []*Statement
+ Elif []IfStatementElif
+ ElseHeaderPos Position
+ ElseHeaderEndPos Position
+ ElseStatements []*Statement
}
// An IfStatementElif holds an elif clause in the if-elif-else statement.
type IfStatementElif struct {
- Condition Expression
- Statements []*Statement
+ HeaderPos Position
+ HeaderEndPos Position
+ Condition Expression
+ Statements []*Statement
}
// An Argument represents an argument to a function definition.
diff --git a/src/parse/asp/grammar_parse.go b/src/parse/asp/grammar_parse.go
index d5bc049bd3..9f582f5c22 100644
--- a/src/parse/asp/grammar_parse.go
+++ b/src/parse/asp/grammar_parse.go
@@ -312,23 +312,25 @@ func (p *parser) parseArgument() Argument {
}
func (p *parser) parseIf() *IfStatement {
- p.nextv("if")
i := &IfStatement{}
+ i.HeaderPos = p.nextv("if").Pos
p.parseExpressionInPlace(&i.Condition)
- p.next(':')
+ i.HeaderEndPos = p.next(':').EndPos()
p.next(EOL)
i.Statements = p.parseStatements()
- for p.optionalv("elif") {
+ for p.l.Peek().Value == "elif" {
elif := IfStatementElif{}
+ elif.HeaderPos = p.nextv("elif").Pos
p.parseExpressionInPlace(&elif.Condition)
- p.next(':')
+ elif.HeaderEndPos = p.next(':').EndPos()
p.next(EOL)
elif.Statements = p.parseStatements()
i.Elif = append(i.Elif, elif)
}
- if p.optionalv("else") {
- p.next(':')
+ if p.l.Peek().Value == "else" {
+ i.ElseHeaderPos = p.nextv("else").Pos
+ i.ElseHeaderEndPos = p.next(':').EndPos()
p.next(EOL)
i.ElseStatements = p.parseStatements()
}
diff --git a/src/parse/asp/interpreter.go b/src/parse/asp/interpreter.go
index 93e4ca0a32..7b165a12c9 100644
--- a/src/parse/asp/interpreter.go
+++ b/src/parse/asp/interpreter.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"iter"
+ "path"
"path/filepath"
"reflect"
"regexp"
@@ -34,6 +35,8 @@ type interpreter struct {
stringMethods, dictMethods, configMethods map[string]*pyFunc
regexCache *cmap.Map[string, *regexp.Regexp]
+ // packageMetadata store scope level metadata for each package.
+ packageMetadata *cmap.Map[core.BuildLabel, scopeMetadata]
}
// newInterpreter creates and returns a new interpreter instance.
@@ -44,6 +47,8 @@ func newInterpreter(state *core.BuildState, p *Parser) *interpreter {
state: state,
locals: map[string]pyObject{},
}
+ s.metadata = &noopScopeMetadata{}
+
i := &interpreter{
scope: s,
parser: p,
@@ -59,6 +64,11 @@ func newInterpreter(state *core.BuildState, p *Parser) *interpreter {
i.subincludes = cmap.NewErrMap[string, pyDict](cmap.SmallShardCount, cmap.XXHash, i.limiter)
i.asts = cmap.NewErrMap[string, []*Statement](cmap.SmallShardCount, cmap.XXHash, i.limiter)
}
+
+ if state.ParseMetadata {
+ i.packageMetadata = cmap.New[core.BuildLabel, scopeMetadata](cmap.SmallShardCount, core.HashBuildLabel)
+ }
+
s.interpreter = i
s.LoadSingletons(state)
return i
@@ -155,7 +165,8 @@ func (i *interpreter) preloadSubinclude(s *scope, label core.BuildLabel) (err er
s.interpreter.loadPluginConfig(s, includeState)
for _, out := range t.FullOutputs() {
- s.SetAll(s.interpreter.Subinclude(s, out, t.Label, true), false)
+ globals := s.interpreter.Subinclude(s, out, t.Label, true)
+ s.SetAllWithOrigin(globals, false, &t.Label)
}
return nil
}
@@ -303,13 +314,16 @@ type scope struct {
pkg *core.Package
subincludeLabel *core.BuildLabel // If set, label of the subinclude we're currently interpreting
parsingFor *parseTarget
- parent *scope
- locals pyDict
- config *pyConfig
- globber *fs.Globber
+ // parent points to the lexical parent of this scope. It is used for variable resolution
+ // and is nil for the root scope.
+ parent *scope
+ locals pyDict
+ config *pyConfig
+ globber *fs.Globber
// True if this scope is for a pre- or post-build callback.
Callback bool
mode core.ParseMode
+ metadata scopeMetadata
}
// parseAnnotatedLabelInPackage similarly to parseLabelInPackage, parses the label contextualising it to the provided
@@ -397,7 +411,7 @@ func (s *scope) subincludePackage() *core.Package {
return pkg
}
// We're probably doing a local subinclude so the package isn't ready yet
- return core.NewPackageSubrepo(s.subincludeLabel.PackageName, s.subincludeLabel.Subrepo)
+ return core.NewPackage(s.subincludeLabel.PackageName, core.WithPackageSubrepo(s.subincludeLabel.Subrepo))
}
return nil
}
@@ -410,7 +424,11 @@ func (s *scope) NewScope(filename string, mode core.ParseMode) *scope {
// NewPackagedScope creates a new child scope of this one pointing to the given package.
// hint is a size hint for the new set of locals.
func (s *scope) NewPackagedScope(pkg *core.Package, mode core.ParseMode, hint int) *scope {
- return s.newScope(pkg, mode, pkg.Filename, hint)
+ newScope := s.newScope(pkg, mode, pkg.Filename, hint)
+ // Since we only want to track metadata for new top level packaged scopes, we explicitly add it here
+ // after creation.
+ newScope.metadata = newScope.getOrNewMetadata(pkg)
+ return newScope
}
func (s *scope) newScope(pkg *core.Package, mode core.ParseMode, filename string, hint int) *scope {
@@ -426,6 +444,9 @@ func (s *scope) newScope(pkg *core.Package, mode core.ParseMode, filename string
config: s.config,
Callback: s.Callback,
mode: mode,
+ // We only track metadata at the top level scope created with [scope.NewPackagedScope]; Every
+ // other child scope or non-packaged (e.g. subincludes) defaults to a noop implementation.
+ metadata: &noopScopeMetadata{},
}
if pkg != nil && pkg.Subrepo != nil && pkg.Subrepo.State != nil {
s2.state = pkg.Subrepo.State
@@ -457,6 +478,7 @@ func (s *scope) NAssert(condition bool, msg string, args ...interface{}) {
// It panics if the variable is not defined.
func (s *scope) Lookup(name string) pyObject {
if obj, present := s.locals[name]; present {
+ s.metadata.pushSymbol(name, s.metadata.origin(s, name))
return obj
} else if s.parent != nil {
return s.parent.Lookup(name)
@@ -480,6 +502,11 @@ func (s *scope) Set(name string, value pyObject) {
// SetAll sets all contents of the given dict in this scope.
// Optionally it can filter to just public objects (i.e. those not prefixed with an underscore)
func (s *scope) SetAll(d pyDict, publicOnly bool) {
+ s.SetAllWithOrigin(d, publicOnly, nil)
+}
+
+// SetAllWithOrigin is like SetAll but also records the origin label for all variables.
+func (s *scope) SetAllWithOrigin(d pyDict, publicOnly bool, origin *core.BuildLabel) {
for k, v := range d {
if k == "CONFIG" {
// Special case; need to merge config entries rather than overwriting the entire object.
@@ -488,6 +515,9 @@ func (s *scope) SetAll(d pyDict, publicOnly bool) {
s.config.Merge(c)
} else if !publicOnly || k[0] != '_' {
s.locals[k] = v
+ if origin != nil {
+ s.metadata.setSymbolOrigin(k, *origin)
+ }
}
}
}
@@ -525,23 +555,24 @@ func (s *scope) interpretStatements(statements []*Statement) pyObject {
}
}()
for _, stmt = range statements {
+ s.metadata.setCursor(stmt)
+ checkpoint := s.metadata.checkpoint()
+
+ var ret pyObject
if stmt.FuncDef != nil {
s.Set(stmt.FuncDef.Name, newPyFunc(s, stmt.FuncDef))
} else if stmt.If != nil {
- if ret := s.interpretIf(stmt.If); ret != nil {
- return ret
- }
+ ret = s.interpretIf(stmt.If)
} else if stmt.For != nil {
- if ret := s.interpretFor(stmt.For); ret != nil {
- return ret
- }
+ ret = s.interpretFor(stmt.For)
} else if stmt.Return != nil {
if len(stmt.Return.Values) == 0 {
- return None
+ ret = None
} else if len(stmt.Return.Values) == 1 {
- return s.interpretExpression(stmt.Return.Values[0])
+ ret = s.interpretExpression(stmt.Return.Values[0])
+ } else {
+ ret = pyList(s.evaluateExpressions(stmt.Return.Values))
}
- return pyList(s.evaluateExpressions(stmt.Return.Values))
} else if stmt.Ident != nil {
s.interpretIdentStatement(stmt.Ident)
} else if stmt.Assert != nil {
@@ -559,15 +590,22 @@ func (s *scope) interpretStatements(statements []*Statement) pyObject {
s.interpretExpression(stmt.Literal)
} else if stmt.Continue {
// This is definitely awkward since we need to control a for loop that's happening in a function outside this scope.
- return continueIteration
+ ret = continueIteration
} else if stmt.Break {
// Similar to above, although CPython does do this the same way...
- return stopIteration
+ ret = stopIteration
} else if stmt.Pass {
- continue // Nothing to do...
+ // Nothing to do...
} else {
s.Error("Unknown statement") // Shouldn't happen, amirite?
}
+
+ s.metadata.registerBuildStatement(s.pkg, stmt)
+ s.metadata.restore(checkpoint)
+
+ if ret != nil {
+ return ret
+ }
}
return nil
}
@@ -1077,6 +1115,22 @@ func (s *scope) Constant(expr *Expression) pyObject {
return nil
}
+// CurrentBuildStatement creates a provider for getting a BuildStatement from the statement
+// that is being currently interpreted. A closure is used to avoid unnecessary computation when the
+// metadata is not being tracked.
+func (s *scope) CurrentBuildStatement() core.BuildStatementProvider {
+ return func() core.BuildStatement {
+ // We lookup the package metadata from the interpreter table no matter how deep we are in the
+ // call stack. We do this for a few reasons:
+ // - to avoid maintaining a caller reference in the scope and using the package as the identifier;
+ // - to avoid a recursive lookup from leaf scopes to the top level package scope. This would likely involve traversing method call scopes generated from subincluded packages;
+ // - enforce the consistency attribute that only one scope metadata exists per package.
+ meta := s.interpreter.packageMetadata.Get(s.pkg.Label())
+ s.NAssert(meta.cursor() == nil, "Cursor is not pointing to a statement")
+ return NewBuildStatement(meta.cursor())
+ }
+}
+
// pkgFilename returns the filename of the current package, or the empty string if there is none.
func (s *scope) pkgFilename() string {
if s.pkg != nil {
@@ -1084,3 +1138,246 @@ func (s *scope) pkgFilename() string {
}
return ""
}
+
+// getOrNewMetadata creates and returns a initialized scopeMetadata instance, or pulls an existing
+// metadata instance for that package from the interpreter package metadata table. It will return
+// a no-op implementation if we simply want to skip tracking for a certain scope.
+func (s *scope) getOrNewMetadata(pkg *core.Package) scopeMetadata {
+ // Skip metadata tracking if:
+ // 1. state.ParseMetadata flag is disabled;
+ // 2. Not interpreting a package (e.g. in subincluded targets)
+ // 3. Any external/remote subrepos.
+ // For 2 and 3, the current uses cases for this metadata (e.g. export) don't process these, so we
+ // avoid tracking to save CPU and memory. That could be easily update if we decide tracking these
+ // is required.
+ var meta scopeMetadata = &noopScopeMetadata{}
+ if pkg == nil || !s.state.ParseMetadata || pkg.Subrepo.IsExternal() {
+ return meta
+ }
+
+ meta, _ = s.interpreter.packageMetadata.AddOrGet(pkg.Label(), func() scopeMetadata {
+ return &trackingScopeMetadata{
+ // symbolOrigins is lazy initialized in [trackingScopeMetadata.setSymbolOrigin]
+ symbolStack: []trackedSymbol{},
+ }
+ })
+ return meta
+}
+
+// getPackageMetadata returns the metadata for that current scope's package.
+func (s *scope) getPackageMetadata() scopeMetadata {
+ if s.pkg == nil || !s.state.ParseMetadata {
+ return &noopScopeMetadata{}
+ }
+ meta := s.interpreter.packageMetadata.Get(s.pkg.Label())
+ if meta == nil {
+ // This can happen for packages we decided to return a noop implementation in
+ // [scope.getOrNewMetadata] and avoided adding to the map due to the unnecessary overhead, it
+ // should translate into a [noopScopeMetadata].
+ return &noopScopeMetadata{}
+ }
+ return meta
+}
+
+// metadataRegisterFiles adds the files to the scope's package metadata.
+func (s *scope) metadataRegisterFiles(rootPath string, files []string) {
+ s.getPackageMetadata().pushFiles(rootPath, files)
+}
+
+// metadataRegisterSubincludes adds the subincluded labels to the scope's package metadata.
+func (s *scope) metadataRegisterSubincludes(labels core.BuildLabels) {
+ s.getPackageMetadata().pushSubincludes(labels)
+}
+
+// scopeMetadata defines an interface for tracking evaluation metadata (such as AST cursor position
+// and symbol subinclude origins) across interpreter scopes.
+// This is optionally used for operations (e.g. export) that require more details on the relation
+// between targets and statements. The no-op implementation should be used for most operations to
+// avoid any computational overhead.
+type scopeMetadata interface {
+ // cursor returns the statement currently being interpreted.
+ cursor() *Statement
+ // origin returns the origin of a symbol given its name. This translates into the label of the
+ // subincluded target which caused that symbol to be loaded into scope. Returns nil if the
+ // symbol is local (defined in the package), preloaded or not tracked.
+ origin(scope *scope, name string) *core.BuildLabel
+ // setCursor registers the statement currently being interpreted.
+ setCursor(stmt *Statement)
+ // registerBuildStatement registers a new Build Statement in the given package. It will also
+ // register the required dependencies for interpreting that statement by looking up the required
+ // origins in the symbol stack.
+ registerBuildStatement(pkg *core.Package, stmt *Statement)
+ // setSymbolOrigin registers the subinclude origin label for a defined symbol.
+ setSymbolOrigin(name string, origin core.BuildLabel)
+ // checkpoint returns a checkpoint for the current tracking stacks.
+ checkpoint() metadataStackCheckpoint
+ // restore truncates the tracking stacks to the specified lengths from the checkpoint.
+ restore(cp metadataStackCheckpoint)
+ // pushSymbol pushes a symbol name and its subinclude origin onto the active tracking stack.
+ pushSymbol(name string, origin *core.BuildLabel)
+ // pushFiles pushes a slice of filenames onto the active tracking stack.
+ pushFiles(rootPath string, filenames []string)
+ // pushSubincludes pushes a label onto the tracking stack for subincludes.
+ pushSubincludes(labels core.BuildLabels)
+}
+
+// trackingScopeMetadata implements the interface [scopeMetadata].
+type trackingScopeMetadata struct {
+ // stmtCursor points to the statement currently being interpreted.
+ stmtCursor *Statement
+ // symbolOrigins tracks the subinclude label that each symbol was originally defined in.
+ symbolOrigins map[string]core.BuildLabel
+ // symbolStack tracks which symbols are actively in use during evaluation.
+ // Symbols are pushed onto the stack during lookups and truncated after each statement.
+ symbolStack []trackedSymbol
+ // fileStack track which files are required during the evaluation of the current statement.
+ // These are pushed during native calls such as glob() and truncated after each top level statement.
+ fileStack []string
+ // subincludesStack track subinclude calls dynamically made while evaluating a statement,
+ // this can happen for example when a custom target (function definition) subincludes a target as
+ // part of the function body. These are pushed during calls to subincludes() and truncated after
+ // each statement.
+ subincludesStack core.BuildLabels
+}
+
+type trackedSymbol struct {
+ name string
+ origin core.BuildLabel
+}
+
+// cursor implements [scopeMetadata.cursor].
+func (m *trackingScopeMetadata) cursor() *Statement {
+ return m.stmtCursor
+}
+
+// origin implements [scopeMetadata.origin].
+func (m *trackingScopeMetadata) origin(scope *scope, name string) *core.BuildLabel {
+ if label, ok := m.symbolOrigins[name]; ok {
+ // Object subincluded into current scope.
+ return &label
+ }
+ // The origin for a local (or subincluded) object is set to nil
+ return nil
+}
+
+// registerBuildStatement implements [scopeMetadata.registerBuildStatement].
+func (m *trackingScopeMetadata) registerBuildStatement(pkg *core.Package, stmt *Statement) {
+ if pkg == nil || stmt == nil {
+ return
+ }
+
+ var requiredSubincludes core.BuildLabels
+ seen := make(map[core.BuildLabel]struct{})
+ for _, l := range m.subincludesStack {
+ if _, ok := seen[l]; !ok {
+ requiredSubincludes = append(requiredSubincludes, l)
+ seen[l] = struct{}{}
+ }
+ }
+ for _, v := range m.symbolStack {
+ l := v.origin
+ if _, ok := seen[l]; !ok {
+ requiredSubincludes = append(requiredSubincludes, l)
+ seen[l] = struct{}{}
+ }
+ }
+
+ pkg.Metadata.RegisterStatement(NewBuildStatement(stmt), requiredSubincludes, m.fileStack)
+}
+
+type metadataStackCheckpoint struct {
+ symbolCheckpoint, fileCheckpoint, subincludesCheckpoint int
+}
+
+// checkpoint implements [scopeMetadata.checkpoint].
+func (m *trackingScopeMetadata) checkpoint() metadataStackCheckpoint {
+ return metadataStackCheckpoint{
+ symbolCheckpoint: len(m.symbolStack),
+ fileCheckpoint: len(m.fileStack),
+ subincludesCheckpoint: len(m.subincludesStack),
+ }
+}
+
+// restore implements [scopeMetadata.restore].
+func (m *trackingScopeMetadata) restore(cp metadataStackCheckpoint) {
+ if cp.symbolCheckpoint >= 0 && cp.symbolCheckpoint <= len(m.symbolStack) {
+ m.symbolStack = m.symbolStack[:cp.symbolCheckpoint]
+ }
+ if cp.fileCheckpoint >= 0 && cp.fileCheckpoint <= len(m.fileStack) {
+ m.fileStack = m.fileStack[:cp.fileCheckpoint]
+ }
+ if cp.subincludesCheckpoint >= 0 && cp.subincludesCheckpoint <= len(m.subincludesStack) {
+ m.subincludesStack = m.subincludesStack[:cp.subincludesCheckpoint]
+ }
+}
+
+// setCursor implements [scopeMetadata.setCursor].
+func (m *trackingScopeMetadata) setCursor(stmt *Statement) {
+ m.stmtCursor = stmt
+}
+
+// setSymbolOrigin implements [scopeMetadata.setSymbolOrigin].
+func (m *trackingScopeMetadata) setSymbolOrigin(name string, origin core.BuildLabel) {
+ if m.symbolOrigins == nil {
+ // Lazy initialization to avoid unnecessary allocation of a map in smaller scopes (no subinclude).
+ m.symbolOrigins = map[string]core.BuildLabel{}
+ }
+
+ m.symbolOrigins[name] = origin
+}
+
+// pushSymbol implements [scopeMetadata.pushSymbol].
+func (m *trackingScopeMetadata) pushSymbol(name string, origin *core.BuildLabel) {
+ if origin == nil {
+ return
+ }
+ m.symbolStack = append(m.symbolStack, trackedSymbol{name: name, origin: *origin})
+}
+
+// pushFiles implements [scopeMetadata.pushFiles].
+func (m *trackingScopeMetadata) pushFiles(rootPath string, filenames []string) {
+ for _, filename := range filenames {
+ m.fileStack = append(m.fileStack, path.Join(rootPath, filename))
+ }
+}
+
+// pushSubincludes implements [scopeMetadata.pushSubincludes].
+func (m *trackingScopeMetadata) pushSubincludes(labels core.BuildLabels) {
+ m.subincludesStack = append(m.subincludesStack, labels...)
+}
+
+// noopScopeMetadata implements the scopeMetadata interface with no-op methods. This is used to
+// avoid the overhead of storing metadata for operations that don't depend on it.
+type noopScopeMetadata struct{}
+
+// cursor implements [scopeMetadata.cursor].
+func (nm *noopScopeMetadata) cursor() *Statement { return nil }
+
+// origin implements [scopeMetadata.origin].
+func (nm *noopScopeMetadata) origin(scope *scope, name string) *core.BuildLabel { return nil }
+
+// registerBuildStatement implements [scopeMetadata.registerBuildStatement].
+func (nm *noopScopeMetadata) registerBuildStatement(pkg *core.Package, stmt *Statement) {}
+
+// checkpoint implements [scopeMetadata.checkpoint].
+func (nm *noopScopeMetadata) checkpoint() metadataStackCheckpoint {
+ return metadataStackCheckpoint{}
+}
+
+// restore implements [scopeMetadata.restore].
+func (nm *noopScopeMetadata) restore(cp metadataStackCheckpoint) {}
+
+// setCursor implements [scopeMetadata.setCursor].
+func (nm *noopScopeMetadata) setCursor(stmt *Statement) {}
+
+// setSymbolOrigin implements [scopeMetadata.setSymbolOrigin].
+func (nm *noopScopeMetadata) setSymbolOrigin(name string, origin core.BuildLabel) {}
+
+// pushSymbol implements [scopeMetadata.pushSymbol].
+func (nm *noopScopeMetadata) pushSymbol(name string, origin *core.BuildLabel) {}
+
+// pushFiles implements [scopeMetadata.pushFiles].
+func (nm *noopScopeMetadata) pushFiles(rootPath string, filenames []string) {}
+
+// pushSubincludes implements [scopeMetadata.pushSubincludes].
+func (nm *noopScopeMetadata) pushSubincludes(labels core.BuildLabels) {}
diff --git a/src/parse/asp/interpreter_test.go b/src/parse/asp/interpreter_test.go
index 511f00b053..dbf1d902c7 100644
--- a/src/parse/asp/interpreter_test.go
+++ b/src/parse/asp/interpreter_test.go
@@ -770,3 +770,131 @@ func TestStrRjust(t *testing.T) {
_, err = parseFile("src/parse/asp/test_data/interpreter/str/rjust_multiple_fillchars.build")
assert.Error(t, err, "fillchar must be exactly one character long")
}
+
+func TestCurrentBuildStatement(t *testing.T) {
+ pkg := core.NewPackage("test/package", core.WithPackageMetadata())
+ pkg.Filename = "test/package/BUILD"
+
+ state := core.NewBuildState(core.DefaultConfiguration())
+ state.ParseMetadata = true
+
+ parser := &Parser{}
+ interpreter := newInterpreter(state, parser)
+
+ rootScope := interpreter.scope.NewPackagedScope(pkg, 0, 0)
+
+ // Root statement in the BUILD file (e.g. a macro call)
+ rootStmt := &Statement{Pos: 10, EndPos: 20}
+ rootScope.metadata.setCursor(rootStmt)
+
+ t.Run("FindsRootStatement", func(t *testing.T) {
+ stmt := rootScope.CurrentBuildStatement()()
+ assert.Equal(t, NewBuildStatement(rootStmt), stmt)
+ })
+}
+
+func TestActiveSubincludes(t *testing.T) {
+ labelA := core.ParseBuildLabel("//pkg:labelA", "")
+ labelB := core.ParseBuildLabel("//pkg:labelB", "")
+ stmt := &Statement{Pos: 1, EndPos: 10}
+
+ t.Run("NoSubincludes", func(t *testing.T) {
+ pkg := core.NewPackage("pkg", core.WithPackageMetadata())
+ meta := newScopeMetadata()
+
+ // Function execution
+ scopeFuncExec := &scope{
+ metadata: meta,
+ }
+ scopeFuncExec.metadata.setCursor(stmt)
+ scopeFuncExec.metadata.registerBuildStatement(pkg, stmt)
+
+ labels, _ := pkg.Metadata.FindPackageLevelRequirements()
+ assert.Empty(t, labels)
+ })
+
+ t.Run("SingleSubinclude", func(t *testing.T) {
+ pkg := core.NewPackage("pkg", core.WithPackageMetadata())
+ meta := newScopeMetadata()
+
+ // File A scope
+ scopeA := &scope{
+ subincludeLabel: &labelA,
+ locals: make(pyDict),
+ metadata: meta,
+ }
+ scopeA.SetAllWithOrigin(pyDict{"foo": pyString("val")}, false, &labelA)
+
+ // Function defined in File A
+ scopeFuncDef := &scope{
+ parent: scopeA,
+ metadata: meta,
+ }
+
+ // Function execution
+ scopeFuncExec := &scope{
+ parent: scopeFuncDef,
+ metadata: meta,
+ }
+
+ // Lookup triggers tracking of required subincludes
+ scopeFuncExec.Lookup("foo")
+
+ scopeFuncExec.metadata.setCursor(stmt)
+ scopeFuncExec.metadata.registerBuildStatement(pkg, stmt)
+
+ labels, _ := pkg.Metadata.FindPackageLevelRequirements()
+ assert.Equal(t, core.BuildLabels{labelA}, labels)
+ })
+
+ t.Run("NestedSubincludes", func(t *testing.T) {
+ pkg := core.NewPackage("pkg", core.WithPackageMetadata())
+ meta := newScopeMetadata()
+
+ // File A scope
+ scopeA := &scope{
+ subincludeLabel: &labelA,
+ locals: make(pyDict),
+ metadata: meta,
+ }
+ scopeA.SetAllWithOrigin(pyDict{"varA": pyString("valA")}, false, &labelA)
+
+ // File B scope (subincluded by A)
+ scopeB := &scope{
+ subincludeLabel: &labelB,
+ parent: scopeA,
+ locals: make(pyDict),
+ metadata: meta,
+ }
+ scopeB.SetAllWithOrigin(pyDict{"varB": pyString("valB")}, false, &labelB)
+
+ // Function defined in File B
+ scopeFuncDef := &scope{
+ parent: scopeB,
+ metadata: meta,
+ }
+
+ // Function execution
+ scopeFuncExec := &scope{
+ parent: scopeFuncDef,
+ metadata: meta,
+ }
+
+ // Lookups trigger tracking of required subincludes
+ scopeFuncExec.Lookup("varA")
+ scopeFuncExec.Lookup("varB")
+
+ scopeFuncExec.metadata.setCursor(stmt)
+ scopeFuncExec.metadata.registerBuildStatement(pkg, stmt)
+
+ labels, _ := pkg.Metadata.FindPackageLevelRequirements()
+ assert.ElementsMatch(t, core.BuildLabels{labelA, labelB}, labels)
+ })
+}
+
+func newScopeMetadata() scopeMetadata {
+ return &trackingScopeMetadata{
+ symbolOrigins: map[string]core.BuildLabel{},
+ symbolStack: []trackedSymbol{},
+ }
+}
diff --git a/src/parse/asp/label_context_test.go b/src/parse/asp/label_context_test.go
index 9b99625bfc..023cb00906 100644
--- a/src/parse/asp/label_context_test.go
+++ b/src/parse/asp/label_context_test.go
@@ -12,7 +12,7 @@ import (
func newScope(pkgName, subrepo, plugin string) *scope {
s := &scope{
- pkg: core.NewPackageSubrepo(pkgName, subrepo),
+ pkg: core.NewPackage(pkgName, core.WithPackageSubrepo(subrepo)),
state: core.NewBuildState(core.DefaultConfiguration()),
}
if plugin != "" {
diff --git a/src/parse/asp/objects.go b/src/parse/asp/objects.go
index a783b55b5c..29dcb3cbfb 100644
--- a/src/parse/asp/objects.go
+++ b/src/parse/asp/objects.go
@@ -698,11 +698,12 @@ func (f *pyFunc) Call(s *scope, c *Call) pyObject {
}
return f.callNative(s, c)
}
- s2 := f.scope.newScope(s.pkg, s.mode, f.scope.filename, len(f.args)+1)
- s2.config = s.config
- s2.Set("CONFIG", s.config) // This needs to be copied across too :(
- s2.Callback = s.Callback
- s2.parsingFor = s.parsingFor
+
+ cs := f.scope.newScope(s.pkg, s.mode, f.scope.filename, len(f.args)+1)
+ cs.config = s.config
+ cs.Set("CONFIG", s.config) // This needs to be copied across too :(
+ cs.Callback = s.Callback
+ cs.parsingFor = s.parsingFor
// Handle implicit 'self' parameter for bound functions.
args := c.Arguments
if f.self != nil {
@@ -720,23 +721,23 @@ func (f *pyFunc) Call(s *scope, c *Call) pyObject {
if present {
name = f.args[idx]
}
- s2.Set(name, f.validateType(s, idx, &a.Value))
+ cs.Set(name, f.validateType(s, idx, &a.Value))
} else {
if i >= len(f.args) {
s.Error("Too many arguments to %s", f.name)
} else if f.kwargsonly {
s.Error("Function %s can only be called with keyword arguments", f.name)
}
- s2.Set(f.args[i], f.validateType(s, i, &a.Value))
+ cs.Set(f.args[i], f.validateType(s, i, &a.Value))
}
}
// Now make sure any arguments with defaults are set, and check any others have been passed.
for i, a := range f.args {
- if s2.LocalLookup(a) == nil {
- s2.Set(a, f.defaultArg(s, i, a))
+ if cs.LocalLookup(a) == nil {
+ cs.Set(a, f.defaultArg(s, i, a))
}
}
- ret := s2.interpretStatements(f.code)
+ ret := cs.interpretStatements(f.code)
if ret == nil {
return None // Implicit 'return None' in any function that didn't do that itself.
}
@@ -832,7 +833,8 @@ func (f *pyFunc) Member(obj pyObject) pyObject {
}
}
-// validateType validates that this argument matches the given type
+// validateType validates that this argument matches the given type. It interprets the expression and
+// returns its value.
func (f *pyFunc) validateType(s *scope, i int, expr *Expression) pyObject {
val := s.interpretExpression(expr)
if i >= len(f.types) && (f.varargs || f.kwargs) {
diff --git a/src/parse/asp/parser.go b/src/parse/asp/parser.go
index f67a8605ae..74a96f0475 100644
--- a/src/parse/asp/parser.go
+++ b/src/parse/asp/parser.go
@@ -35,14 +35,14 @@ type Parser struct {
// NewParser creates a new parser instance. One is normally sufficient for a process lifetime.
func NewParser(state *core.BuildState) *Parser {
- p := newParser()
+ p := NewParserOnly()
p.interpreter = newInterpreter(state, p)
p.limiter = p.interpreter.limiter
return p
}
-// newParser creates just the parser with no interpreter.
-func newParser() *Parser {
+// NewParserOnly creates just the parser with no interpreter.
+func NewParserOnly() *Parser {
return &Parser{
builtins: map[string][]byte{},
limiter: make(semaphore, 10),
diff --git a/src/parse/asp/parser_test.go b/src/parse/asp/parser_test.go
index fd534b31bc..1089bcc012 100644
--- a/src/parse/asp/parser_test.go
+++ b/src/parse/asp/parser_test.go
@@ -10,7 +10,7 @@ import (
// TODO(peterebden): Might get rid of this, we may want to expose a similar thing on Parser.
func parseFileOnly(filename string) (*File, []*Statement, error) {
- stmts, err := newParser().ParseFileOnly(filename)
+ stmts, err := NewParserOnly().ParseFileOnly(filename)
return newFile(filename), stmts, err
}
@@ -448,7 +448,7 @@ func TestAssert(t *testing.T) {
}
func TestOptimise(t *testing.T) {
- p := newParser()
+ p := NewParserOnly()
statements, err := p.parse(nil, "src/parse/asp/test_data/optimise.build")
f := newFile("src/parse/asp/test_data/optimise.build")
assert.NoError(t, err)
@@ -486,7 +486,7 @@ func TestOptimise(t *testing.T) {
}
func TestOptimiseJoin(t *testing.T) {
- p := newParser()
+ p := NewParserOnly()
statements, err := p.parse(nil, "src/parse/asp/test_data/optimise_join.build")
assert.NoError(t, err)
assert.Equal(t, 1, len(statements))
@@ -658,12 +658,12 @@ func TestMissingNewlines(t *testing.T) {
}
func TestRepeatedArguments(t *testing.T) {
- _, err := newParser().parse(nil, "src/parse/asp/test_data/repeated_arguments.build")
+ _, err := NewParserOnly().parse(nil, "src/parse/asp/test_data/repeated_arguments.build")
assert.Error(t, err)
}
func TestConstantAssignments(t *testing.T) {
- _, err := newParser().parse(nil, "src/parse/asp/test_data/constant_assign.build")
+ _, err := NewParserOnly().parse(nil, "src/parse/asp/test_data/constant_assign.build")
assert.Error(t, err)
}
@@ -815,7 +815,7 @@ func TestFStringConcat(t *testing.T) {
func TestFStringImplicitStringConcat(t *testing.T) {
str := "str('testing that we can carry these ' f'over {multiple} lines' r' \\n')"
- prog, err := newParser().parseAndHandleErrors(strings.NewReader(strings.ReplaceAll(str, "\t", "")))
+ prog, err := NewParserOnly().parseAndHandleErrors(strings.NewReader(strings.ReplaceAll(str, "\t", "")))
require.NoError(t, err)
fString := prog[0].Ident.Action.Call.Arguments[0].Value.Val.FString
@@ -827,21 +827,21 @@ func TestFStringImplicitStringConcat(t *testing.T) {
// F strings should report a sensible error when the {} aren't complete
func TestFStringIncompleteError(t *testing.T) {
str := "s = f'some {' '.join([])}'"
- _, err := newParser().parseAndHandleErrors(strings.NewReader(str))
+ _, err := NewParserOnly().parseAndHandleErrors(strings.NewReader(str))
require.Error(t, err)
assert.Contains(t, err.Error(), "Unterminated brace in fstring")
}
// Continue shouldn't be allowed outside a loop
func TestContinueOutsideLoop(t *testing.T) {
- _, err := newParser().parseAndHandleErrors(strings.NewReader("continue"))
+ _, err := NewParserOnly().parseAndHandleErrors(strings.NewReader("continue"))
require.Error(t, err)
assert.Contains(t, err.Error(), "'continue' outside loop")
}
// Break shouldn't be allowed outside a loop
func TestBreakOutsideLoop(t *testing.T) {
- _, err := newParser().parseAndHandleErrors(strings.NewReader("break"))
+ _, err := NewParserOnly().parseAndHandleErrors(strings.NewReader("break"))
require.Error(t, err)
assert.Contains(t, err.Error(), "'break' outside loop")
}
@@ -853,7 +853,7 @@ for i in [1,2,3]:
def foo():
break
`
- _, err := newParser().parseAndHandleErrors(strings.NewReader(code))
+ _, err := NewParserOnly().parseAndHandleErrors(strings.NewReader(code))
require.Error(t, err)
assert.Contains(t, err.Error(), "'break' outside loop")
}
diff --git a/src/parse/parse_step.go b/src/parse/parse_step.go
index 2c9689feab..6c9ec6f11d 100644
--- a/src/parse/parse_step.go
+++ b/src/parse/parse_step.go
@@ -68,7 +68,7 @@ func parse(state *core.BuildState, label, dependent core.BuildLabel, mode core.P
// If we get here then it falls to us to parse this package.
state.LogParseResult(label, core.PackageParsing, "Parsing...")
- if subrepo != nil && subrepo.Target != nil {
+ if subrepo.IsExternal() {
// We have got the definition of the subrepo, but it depends on something, make sure that has been built.
state.WaitForBuiltTarget(subrepo.Target.Label, label, mode|core.ParseModeForSubinclude)
if !subrepo.Target.State().IsBuilt() {
@@ -90,6 +90,12 @@ func parse(state *core.BuildState, label, dependent core.BuildLabel, mode core.P
}
state.LogParseResult(label, core.PackageParsed, "Parsed package")
+ if state.ForceParseEntirePackage && !subrepo.IsExternal() && !mode.IsForSubinclude() {
+ if err := state.QueueEntirePackage(pkg, label, dependent, mode); err != nil {
+ return err
+ }
+ }
+
// The target likely got activated already, however we activate here to handle pseudo-targets (:all), and to let
// this error when the target doesn't exist.
return state.ActivateTarget(pkg, label, dependent, mode)
@@ -182,7 +188,12 @@ func maybeParseSubrepoPackage(state *core.BuildState, subrepoPkg, subrepoSubrepo
// parsePackage parses a BUILD file and adds the package to the build graph
func parsePackage(state *core.BuildState, label, dependent core.BuildLabel, subrepo *core.Subrepo, mode core.ParseMode) (*core.Package, error) {
packageName := label.PackageName
- pkg := core.NewPackage(packageName)
+ var opts []core.PackageOptions
+ if state.ParseMetadata && !subrepo.IsExternal() {
+ // Skip metadata tracking for external subrepos since these are always used as is and never trimmed.
+ opts = append(opts, core.WithPackageMetadata())
+ }
+ pkg := core.NewPackage(packageName, opts...)
pkg.Subrepo = subrepo
var fileSystem iofs.FS = fs.HostFS
if subrepo != nil {
diff --git a/src/please.go b/src/please.go
index 9757c50244..d53c2903cb 100644
--- a/src/please.go
+++ b/src/please.go
@@ -462,6 +462,17 @@ var opts struct {
Options []string `positional-arg-name:"options" description:"Print specific options."`
} `positional-args:"true"`
} `command:"config" description:"Prints the configuration settings"`
+ Metadata struct {
+ Sources bool `long:"sources" short:"s" description:"Include target sources in visualization"`
+ Deps bool `long:"deps" short:"d" description:"Include target dependencies in visualization"`
+ Outputs bool `long:"outputs" short:"o" description:"Include target outputs in visualization"`
+ All bool `long:"all" short:"a" description:"Include all details (sources, deps, and outputs)"`
+ AllStatements bool `long:"all_statements" description:"Print all statements in the BUILD file, including those that didn't generate the specified targets"`
+ Hidden bool `long:"hidden" description:"Show hidden targets"`
+ Args struct {
+ Targets []core.BuildLabel `positional-arg-name:"targets" description:"Targets or packages to display metadata for" required:"true"`
+ } `positional-args:"true" required:"true"`
+ } `command:"metadata" description:"Prints all metadata (code statements, generated targets and their required subincludes/files) of a package."`
} `command:"query" description:"Queries information about the build state"`
Generate struct {
Gitignore string `long:"update_gitignore" description:"The gitignore file to write the generated sources to"`
@@ -475,7 +486,7 @@ var opts struct {
// Functions are called after args are parsed and return a POSIX exit code (0 means success).
var buildFunctions = map[string]func() int{
"build": func() int {
- success, state := runBuild(opts.Build.Args.Targets, true, false, false)
+ success, state := runBuild(opts.Build.Args.Targets, buildOpts{Build: true})
if !success || opts.Build.OutDir == "" {
return toExitCode(success, state)
}
@@ -499,7 +510,7 @@ var buildFunctions = map[string]func() int{
if opts.Hash.Update {
opts.BehaviorFlags.NoHashVerification = true
}
- success, state := runBuild(opts.Hash.Args.Targets, true, false, false)
+ success, state := runBuild(opts.Hash.Args.Targets, buildOpts{Build: true})
if success {
if opts.Hash.Detailed {
for _, target := range state.ExpandOriginalLabels() {
@@ -562,14 +573,14 @@ var buildFunctions = map[string]func() int{
return toExitCode(success, state)
},
"debug": func() int {
- success, state := runBuild([]core.BuildLabel{opts.Debug.Args.Target}, true, false, false)
+ success, state := runBuild([]core.BuildLabel{opts.Debug.Args.Target}, buildOpts{Build: true})
if !success {
return toExitCode(success, state)
}
return debug.Debug(state, opts.Debug.Args.Target, opts.Debug.Args.Args, exec.ConvertEnv(opts.Debug.Env), opts.Debug.Share.Network, opts.Debug.Share.Mount)
},
"exec": func() int {
- success, state := runBuild([]core.BuildLabel{opts.Exec.Args.Target.BuildLabel}, true, false, false)
+ success, state := runBuild([]core.BuildLabel{opts.Exec.Args.Target.BuildLabel}, buildOpts{Build: true})
if !success {
return toExitCode(success, state)
}
@@ -600,7 +611,7 @@ var buildFunctions = map[string]func() int{
if len(unannotated) == 0 {
return 0
}
- success, state := runBuild(unannotated, true, false, false)
+ success, state := runBuild(unannotated, buildOpts{Build: true})
if !success {
return toExitCode(success, state)
}
@@ -614,7 +625,7 @@ var buildFunctions = map[string]func() int{
if len(unannotated) == 0 {
return 0
}
- success, state := runBuild(unannotated, true, false, false)
+ success, state := runBuild(unannotated, buildOpts{Build: true})
if !success {
return toExitCode(success, state)
}
@@ -624,7 +635,7 @@ var buildFunctions = map[string]func() int{
return 0
},
"run": func() int {
- if success, state := runBuild([]core.BuildLabel{opts.Run.Args.Target.BuildLabel}, true, false, false); success {
+ if success, state := runBuild([]core.BuildLabel{opts.Run.Args.Target.BuildLabel}, buildOpts{Build: true}); success {
var dir string
if opts.Run.WD != "" {
dir = getAbsolutePath(opts.Run.WD, originalWorkingDirectory)
@@ -648,7 +659,7 @@ var buildFunctions = map[string]func() int{
if len(unannotated) == 0 {
return 0
}
- if success, state := runBuild(unannotated, true, false, false); success {
+ if success, state := runBuild(unannotated, buildOpts{Build: true}); success {
var dir string
if opts.Run.WD != "" {
dir = getAbsolutePath(opts.Run.WD, originalWorkingDirectory)
@@ -665,7 +676,7 @@ var buildFunctions = map[string]func() int{
if len(unannotated) == 0 {
return 0
}
- if success, state := runBuild(unannotated, true, false, false); success {
+ if success, state := runBuild(unannotated, buildOpts{Build: true}); success {
var dir string
if opts.Run.WD != "" {
dir = getAbsolutePath(opts.Run.WD, originalWorkingDirectory)
@@ -692,7 +703,7 @@ var buildFunctions = map[string]func() int{
}
opts.Clean.Args.Targets = core.WholeGraph
}
- if success, state := runBuild(opts.Clean.Args.Targets, false, false, false); success {
+ if success, state := runBuild(opts.Clean.Args.Targets, buildOpts{}); success {
clean.Targets(state, state.ExpandOriginalLabels())
return 0
}
@@ -714,7 +725,7 @@ var buildFunctions = map[string]func() int{
return 1
},
"gc": func() int {
- success, state := runBuild(core.WholeGraph, false, false, true)
+ success, state := runBuild(core.WholeGraph, buildOpts{IsQuery: true})
if success {
gc.GarbageCollect(state, opts.Gc.Args.Targets, state.ExpandLabels(state.Config.Gc.Keep), state.Config.Gc.Keep, state.Config.Gc.KeepLabel,
opts.Gc.Conservative, opts.Gc.TargetsOnly, opts.Gc.SrcsOnly, opts.Gc.NoPrompt, opts.Gc.DryRun, opts.Gc.Git)
@@ -773,14 +784,16 @@ var buildFunctions = map[string]func() int{
return 0
},
"export": func() int {
- success, state := runBuild(opts.Export.Args.Targets, false, false, false)
+ success, state := runBuild(opts.Export.Args.Targets, buildOpts{ParseMetadata: true, ForceParseEntirePackage: true})
+
if success {
- export.ToDir(state, opts.Export.Output, opts.Export.NoTrim, state.ExpandOriginalLabels())
+ export.Repo(state, opts.Export.Output, opts.Export.NoTrim, state.ExpandOriginalLabels())
}
+
return toExitCode(success, state)
},
"export.outputs": func() int {
- success, state := runBuild(opts.Export.Outputs.Args.Targets, true, false, true)
+ success, state := runBuild(opts.Export.Outputs.Args.Targets, buildOpts{Build: true, IsQuery: true})
if success {
export.Outputs(state, opts.Export.Output, state.ExpandOriginalLabels())
}
@@ -838,6 +851,22 @@ var buildFunctions = map[string]func() int{
query.TargetOutputs(state.Graph, state.ExpandOriginalLabels(), opts.Query.Output.JSON)
})
},
+ "query.metadata": func() int {
+ if success, state := runBuild(opts.Query.Metadata.Args.Targets, buildOpts{ParseMetadata: true, IsQuery: true}); success {
+ m := opts.Query.Metadata
+ query.Metadata(state, state.ExpandOriginalLabels(), query.MetadataOpts{
+ TargetDetailsOpts: query.TargetDetailsOpts{
+ IncludeSources: m.Sources || m.All,
+ IncludeDeps: m.Deps || m.All,
+ IncludeOutputs: m.Outputs || m.All,
+ },
+ IncludeAllStatements: m.AllStatements,
+ ShowHidden: m.Hidden,
+ })
+ return 0
+ }
+ return 1
+ },
"query.completions": func() int {
// Somewhat fiddly because the inputs are not necessarily well-formed at this point.
opts.ParsePackageOnly = true
@@ -968,14 +997,14 @@ var buildFunctions = map[string]func() int{
log.Fatalf("%s", err)
}
readConfig()
- _, before := runBuild(core.WholeGraph, false, false, false)
+ _, before := runBuild(core.WholeGraph, buildOpts{})
// N.B. Ignore failure here; if we can't parse the graph before then it will suffice to
// assume that anything we don't know about has changed.
if err := scm.Checkout(original); err != nil {
log.Fatalf("%s", err)
}
readConfig()
- success, after := runBuild(core.WholeGraph, false, false, false)
+ success, after := runBuild(core.WholeGraph, buildOpts{})
if !success {
return 1
}
@@ -1007,7 +1036,7 @@ var buildFunctions = map[string]func() int{
"watch": func() int {
targets, args := testTargets(opts.Watch.Args.Target, opts.Watch.Args.Args, false, "")
// Don't ask it to test now since we don't know if any of them are tests yet.
- success, state := runBuild(targets, true, false, false)
+ success, state := runBuild(targets, buildOpts{Build: true})
state.NeedRun = opts.Watch.Run
watch.Watch(state, state.ExpandOriginalLabels(), args, opts.Watch.NoTest, runPlease)
return toExitCode(success, state)
@@ -1032,7 +1061,7 @@ var buildFunctions = map[string]func() int{
opts.Generate.Args.Targets = []core.BuildLabel{target}
}
- if success, state := runBuild(opts.Generate.Args.Targets, true, false, true); success {
+ if success, state := runBuild(opts.Generate.Args.Targets, buildOpts{Build: true, IsQuery: true}); success {
if opts.Generate.Gitignore != "" {
err := generate.UpdateGitignore(state.Graph, state.ExpandOriginalLabels(), opts.Generate.Gitignore)
if err != nil {
@@ -1074,7 +1103,7 @@ func runTool(_tool tool.Tool) int {
// We skip loading the repo config in init for `plz tool` to allow this command to work outside of a repo root. If
// the tool looks like a build label, we need to set the repo root now.
config = mustReadConfigAndSetRoot(false)
- if success, state := runBuild(label, true, false, false); success {
+ if success, state := runBuild(label, buildOpts{Build: true}); success {
annotatedOutputLabels := core.AnnotateLabels(label)
run.Run(state, annotatedOutputLabels[0], opts.Tool.Args.Args.AsStrings(), false, false, false, "", "")
}
@@ -1106,7 +1135,7 @@ func runQuery(needFullParse bool, labels []core.BuildLabel, onSuccess func(state
if len(labels) == 0 {
labels = core.WholeGraph
}
- if success, state := runBuild(labels, false, false, true); success {
+ if success, state := runBuild(labels, buildOpts{IsQuery: true}); success {
onSuccess(state)
return 0
}
@@ -1118,7 +1147,7 @@ func doTest(targets []core.BuildLabel, args []string, surefireDir cli.Filepath,
fs.RemoveAll(string(resultsFile))
os.MkdirAll(string(surefireDir), core.DirPermissions)
opts.Test.StateArgs = args
- success, state := runBuild(targets, true, true, false)
+ success, state := runBuild(targets, buildOpts{Build: true, Test: true})
test.CopySurefireXMLFilesToDir(state, string(surefireDir))
test.WriteResultsToFileOrDie(state.Graph, string(resultsFile), state.Config.Test.StoreTestOutputOnSuccess)
return success, state
@@ -1133,7 +1162,7 @@ func prettyOutput(interactiveOutput bool, plainOutput bool, verbosity cli.Verbos
}
// Please starts & runs the main build process through to its completion.
-func Please(targets []core.BuildLabel, config *core.Configuration, shouldBuild, shouldTest bool) (bool, *core.BuildState) {
+func Please(targets []core.BuildLabel, config *core.Configuration, buildOpts buildOpts) (bool, *core.BuildState) {
if opts.BuildFlags.NumThreads > 0 {
config.Please.NumThreads = opts.BuildFlags.NumThreads
config.Parse.NumThreads = opts.BuildFlags.NumThreads
@@ -1156,8 +1185,6 @@ func Please(targets []core.BuildLabel, config *core.Configuration, shouldBuild,
state.TestSequentially = opts.Test.Sequentially || opts.Cover.Sequentially // Similarly here.
state.TestArgs = opts.Test.StateArgs
state.NeedCoverage = opts.Cover.active || config.Build.Config == "cover"
- state.NeedBuild = shouldBuild
- state.NeedTests = shouldTest
state.NeedRun = !opts.Run.Args.Target.IsEmpty() || len(opts.Run.Parallel.PositionalArgs.Targets) > 0 || len(opts.Run.Sequential.PositionalArgs.Targets) > 0 || !opts.Exec.Args.Target.IsEmpty() || len(opts.Exec.Sequential.Args.Targets) > 0 || len(opts.Exec.Parallel.Args.Targets) > 0 || opts.Tool.Args.Tool != "" || debug
state.NeedHashesOnly = len(opts.Hash.Args.Targets) > 0
state.PrepareOnly = opts.Build.Shell != "" || opts.Test.Shell != "" || opts.Cover.Shell != ""
@@ -1171,6 +1198,10 @@ func Please(targets []core.BuildLabel, config *core.Configuration, shouldBuild,
state.ShowAllOutput = opts.OutputFlags.ShowAllOutput
state.ParsePackageOnly = opts.ParsePackageOnly
state.EnableBreakpoints = opts.BehaviorFlags.Debug
+ state.NeedBuild = buildOpts.Build
+ state.NeedTests = buildOpts.Test
+ state.ParseMetadata = buildOpts.ParseMetadata
+ state.ForceParseEntirePackage = buildOpts.ForceParseEntirePackage
state.NeedDebugDeps = debug
// What outputs get downloaded in remote execution.
@@ -1323,10 +1354,23 @@ func readConfig() *core.Configuration {
return cfg
}
+// buildOpts specifies parameter for the core.runBuild method.
+type buildOpts struct {
+ Build bool
+ Test bool
+ IsQuery bool
+ // ParseMetadata is true if we want to store BUILD file metadata during parsing.
+ ParseMetadata bool
+ // ForceParseEntirePackage is true if we want to force parse and activate all targets in every
+ // visited package. This is required to include adjacent targets in the build graph for operations
+ // like export.
+ ForceParseEntirePackage bool
+}
+
// Runs the actual build
// Which phases get run are controlled by shouldBuild and shouldTest.
-func runBuild(targets []core.BuildLabel, shouldBuild, shouldTest, isQuery bool) (bool, *core.BuildState) {
- if !isQuery {
+func runBuild(targets []core.BuildLabel, buildOpts buildOpts) (bool, *core.BuildState) {
+ if !buildOpts.IsQuery {
opts.BuildFlags.Exclude = append(opts.BuildFlags.Exclude, "manual", "manual:"+core.OsArch)
}
if stat, _ := os.Stdin.Stat(); (stat.Mode()&os.ModeCharDevice) == 0 && !plz.ReadingStdin(targets) {
@@ -1338,7 +1382,7 @@ func runBuild(targets []core.BuildLabel, shouldBuild, shouldTest, isQuery bool)
if len(targets) == 0 {
targets = core.InitialPackage()
}
- return Please(targets, config, shouldBuild, shouldTest)
+ return Please(targets, config, buildOpts)
}
var originalWorkingDirectory string
@@ -1436,7 +1480,7 @@ func getCompletions(qry string) (*query.CompletionPackages, []string) {
if completions.PackageToParse != "" || completions.IsRoot {
labelsToParse := []core.BuildLabel{{PackageName: completions.PackageToParse, Name: "all"}}
- if success, state := Please(labelsToParse, config, false, false); success {
+ if success, state := Please(labelsToParse, config, buildOpts{}); success {
return completions, query.Completions(state.Graph, completions, binary, isTest, completions.Hidden)
}
}
diff --git a/src/plz/plz.go b/src/plz/plz.go
index 7c893a8165..4cbbd10454 100644
--- a/src/plz/plz.go
+++ b/src/plz/plz.go
@@ -110,7 +110,7 @@ func Run(targets, preTargets []core.BuildLabel, state *core.BuildState, config *
}
wg.Done()
}()
- // Wait until they've all exited, which they'll do once they have no tasks left.
+ // Wait for all workers to finish. This should happen as soon as we no longer have pending tasks.
wg.Wait()
if state.Cache != nil {
state.Cache.Shutdown()
diff --git a/src/query/BUILD b/src/query/BUILD
index 310bd21794..26a780b6a0 100644
--- a/src/query/BUILD
+++ b/src/query/BUILD
@@ -10,6 +10,7 @@ go_library(
"///third_party/go/github.com_please-build_gcfg//:gcfg",
"///third_party/go/golang.org_x_exp//maps",
"//src/build",
+ "//src/cli",
"//src/cli/logging",
"//src/core",
"//src/fs",
diff --git a/src/query/changes_test.go b/src/query/changes_test.go
index ed4124b684..d3806a4527 100644
--- a/src/query/changes_test.go
+++ b/src/query/changes_test.go
@@ -160,7 +160,7 @@ func addTarget(state *core.BuildState, label string, dep *core.BuildTarget, sour
}
pkg := state.Graph.PackageByLabel(t.Label)
if pkg == nil {
- pkg = core.NewPackageSubrepo(t.Label.PackageName, t.Label.Subrepo)
+ pkg = core.NewPackage(t.Label.PackageName, core.WithPackageSubrepo(t.Label.Subrepo))
state.Graph.AddPackage(pkg)
}
pkg.AddTarget(t)
diff --git a/src/query/metadata.go b/src/query/metadata.go
new file mode 100644
index 0000000000..6fb18de780
--- /dev/null
+++ b/src/query/metadata.go
@@ -0,0 +1,231 @@
+package query
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/thought-machine/please/src/cli"
+ "github.com/thought-machine/please/src/core"
+)
+
+// MetadataOpts contains configuration options for formatting and writing package metadata.
+type MetadataOpts struct {
+ TargetDetailsOpts
+ IncludeAllStatements bool
+ ShowHidden bool
+}
+
+// TargetDetailsOpts contains configuration options for specifying which additional details (sources,
+// dependencies, or outputs) should be included when displaying target metadata.
+type TargetDetailsOpts struct {
+ IncludeSources bool
+ IncludeDeps bool
+ IncludeOutputs bool
+}
+
+// anyTargetDetails reports true if any of the additional target options are set to true.
+func (o TargetDetailsOpts) anyTargetDetails() bool {
+ return o.IncludeSources || o.IncludeDeps || o.IncludeOutputs
+}
+
+// Metadata prints out a visualization of the parsed build statement metadata for the given targets.
+func Metadata(state *core.BuildState, targets core.BuildLabels, opts MetadataOpts) {
+ if !cli.ShowColouredOutput || !cli.IsATerminal(os.Stdout) {
+ cli.ShowColouredOutput = false
+ }
+
+ // Group requested targets by their package
+ packageTargets := map[*core.Package]core.BuildLabels{}
+ for _, label := range targets {
+ pkg := state.Graph.PackageOrDie(label)
+ packageTargets[pkg] = append(packageTargets[pkg], label)
+ }
+
+ printMetadata(state, packageTargets, opts)
+}
+
+// printMetadata formats and draws the metadata as a colorized terminal tree-box layout.
+func printMetadata(state *core.BuildState, packageTargets map[*core.Package]core.BuildLabels, opts MetadataOpts) {
+ // itemDetail holds the text to print and its optional color formatting string
+ type itemDetail struct {
+ text string
+ color string
+ }
+
+ // Helper to print a titled section of items using precise tree box-drawing characters
+ printSection := func(prefix, title string, items []itemDetail, isLast bool) string {
+ if len(items) == 0 {
+ return ""
+ }
+ branch := "├──"
+ childPrefix := prefix + "│ "
+ if isLast {
+ branch = "└──"
+ childPrefix = prefix + " "
+ }
+ cli.Fprintf(os.Stdout, "%s%s ${CYAN}%s:${RESET}\n", prefix, branch, title)
+ for idx, item := range items {
+ itemBranch := "├──"
+ if idx == len(items)-1 {
+ itemBranch = "└──"
+ }
+ cli.Fprintf(os.Stdout, "%s%s "+item.color+"%s${RESET}\n", childPrefix, itemBranch, item.text)
+ }
+ return childPrefix
+ }
+
+ labelsToItems := func(labels core.BuildLabels, defaultColor string) []itemDetail {
+ res := make([]itemDetail, len(labels))
+ for i, l := range labels {
+ res[i] = itemDetail{text: l.String(), color: defaultColor}
+ }
+ return res
+ }
+
+ stringsToItems := func(strs []string, defaultColor string) []itemDetail {
+ res := make([]itemDetail, len(strs))
+ for i, s := range strs {
+ res[i] = itemDetail{text: s, color: defaultColor}
+ }
+ return res
+ }
+
+ inputsToItems := func(inputs []core.BuildInput) []itemDetail {
+ res := make([]itemDetail, len(inputs))
+ for i, inp := range inputs {
+ color := ""
+ if _, ok := inp.Label(); ok {
+ color = "${GREEN}"
+ }
+ res[i] = itemDetail{text: inp.String(), color: color}
+ }
+ return res
+ }
+
+ for pkg, labels := range packageTargets {
+ fmt.Printf("=== Package: %s (File: %s) ===\n", pkg.Label(), pkg.Filename)
+
+ content, err := os.ReadFile(pkg.Filename)
+ if err != nil {
+ fmt.Printf("Error reading BUILD file %s: %v\n\n", pkg.Filename, err)
+ continue
+ }
+
+ trackedStmts := pkg.Metadata.Statements()
+ var filterStmts map[core.BuildStatement]struct{}
+ if !opts.IncludeAllStatements {
+ filterStmts = filterStatements(pkg, labels)
+ }
+
+ for _, ts := range trackedStmts {
+ stmt := ts.Statement
+ sm := ts.Metadata
+ if filterStmts != nil {
+ if _, ok := filterStmts[stmt]; !ok {
+ continue
+ }
+ }
+
+ code := string(content[stmt.StartPos():stmt.EndPos()])
+
+ cli.Fprintf(os.Stdout, "${BOLD_CYAN}Statement (Offsets: %d-%d):${RESET}\n",
+ stmt.StartPos(), stmt.EndPos())
+ cli.Fprintf(os.Stdout, " ${CYAN}Code:${RESET}\n")
+ // Indent the code
+ for line := range strings.SplitSeq(code, "\n") {
+ cli.Fprintf(os.Stdout, " %s\n", line)
+ }
+
+ targets := sm.Targets
+ if !opts.ShowHidden {
+ targets = make(core.BuildLabels, 0, len(sm.Targets))
+ for _, t := range sm.Targets {
+ if !t.IsHidden() {
+ targets = append(targets, t)
+ }
+ }
+ }
+
+ hasSubincludes := len(sm.Subincludes) > 0
+ hasFiles := len(sm.Files) > 0
+ hasTargets := len(targets) > 0
+
+ var lastSection string
+ if hasTargets {
+ lastSection = "targets"
+ } else if hasFiles {
+ lastSection = "files"
+ } else if hasSubincludes {
+ lastSection = "subincludes"
+ }
+
+ basePrefix := " "
+ if hasSubincludes {
+ printSection(basePrefix, "Required Subincludes", labelsToItems(sm.Subincludes, "${YELLOW}"), lastSection == "subincludes")
+ }
+
+ if hasFiles {
+ printSection(basePrefix, "Required Files", stringsToItems(sm.Files, ""), lastSection == "files")
+ }
+
+ if hasTargets {
+ branch := "├──"
+ childPrefix := basePrefix + "│ "
+ if lastSection == "targets" {
+ branch = "└──"
+ childPrefix = basePrefix + " "
+ }
+ cli.Fprintf(os.Stdout, "%s%s ${CYAN}Generated Targets:${RESET}\n", basePrefix, branch)
+
+ for i, t := range targets {
+ targetBranch := "├──"
+ targetChildPrefix := childPrefix + "│ "
+ if i == len(targets)-1 {
+ targetBranch = "└──"
+ targetChildPrefix = childPrefix + " "
+ }
+ cli.Fprintf(os.Stdout, "%s%s ${BOLD_GREEN}%s${RESET}\n", childPrefix, targetBranch, t)
+
+ if opts.anyTargetDetails() {
+ if target := state.Graph.Target(t); target != nil {
+ type optDetail struct {
+ title string
+ items []itemDetail
+ }
+ var details []optDetail
+ if opts.IncludeSources && len(target.AllSources()) > 0 {
+ details = append(details, optDetail{"Sources", inputsToItems(target.AllSources())})
+ }
+ if opts.IncludeDeps && len(target.DeclaredDependencies()) > 0 {
+ details = append(details, optDetail{"Dependencies", labelsToItems(target.DeclaredDependencies(), "${GREEN}")})
+ }
+ if opts.IncludeOutputs && len(target.Outputs()) > 0 {
+ details = append(details, optDetail{"Outputs", stringsToItems(target.Outputs(), "")})
+ }
+
+ for idx, det := range details {
+ isLastDetail := idx == len(details)-1
+ printSection(targetChildPrefix, det.title, det.items, isLastDetail)
+ }
+ }
+ }
+ }
+ }
+ fmt.Fprintln(os.Stdout)
+ }
+ }
+}
+
+// filterStatements retrieves the build statements corresponding to the given labels, restricting
+// metadata printing to only the statements that define these targets.
+func filterStatements(pkg *core.Package, labels core.BuildLabels) map[core.BuildStatement]struct{} {
+ filterStmts := map[core.BuildStatement]struct{}{}
+ for _, target := range labels {
+ stmt, err := pkg.Metadata.FindStatement(target)
+ if err == nil && stmt != nil {
+ filterStmts[stmt] = struct{}{}
+ }
+ }
+ return filterStmts
+}
diff --git a/src/query/reverse_deps.go b/src/query/reverse_deps.go
index cca1ea2f4c..f9ba93e3c3 100644
--- a/src/query/reverse_deps.go
+++ b/src/query/reverse_deps.go
@@ -123,7 +123,7 @@ func buildRevdeps(graph *core.BuildGraph, includeSubrepos bool) map[core.BuildLa
// Targets in a subrepo don't express an explicit dependency on their subrepo's target.
// However this is often useful for query commands where you expect to see this kind of
// relationship. Hence, if requested, we add the extra 'dependency' here.
- if includeSubrepos && t.Subrepo != nil && t.Subrepo.Target != nil {
+ if includeSubrepos && t.Subrepo.IsExternal() {
revdeps[t.Subrepo.Target.Label] = append(revdeps[t.Subrepo.Target.Label], t)
}
}
diff --git a/src/query/somepath.go b/src/query/somepath.go
index 75b91bdc10..adc3b9a8bf 100644
--- a/src/query/somepath.go
+++ b/src/query/somepath.go
@@ -104,7 +104,7 @@ func somePath(graph *core.BuildGraph, target1, target2 *core.BuildTarget, seen,
}
}
}
- if target1.Subrepo != nil && target1.Subrepo.Target != nil {
+ if target1.Subrepo.IsExternal() {
if path := somePath(graph, target1.Subrepo.Target, target2, seen, except); len(path) != 0 {
return append([]core.BuildLabel{target1.Label}, path...)
}
diff --git a/test/export/BUILD b/test/export/BUILD
index 545dc35c80..bbd0e99020 100644
--- a/test/export/BUILD
+++ b/test/export/BUILD
@@ -9,5 +9,21 @@ filegroup(
# Generic catch-all test on internal repo.
plz_e2e_test(
name = "export_src_please_test",
- cmd = "plz export --output plz-out/plzexport //src/core && plz --repo_root=$(plz query reporoot)/plz-out/plzexport build //src/core",
+ cmd = " && ".join([
+ 'CACHE_CONF="--override=cache.dir:$(plz query reporoot)/plz-out/test-cache"',
+ 'plz "$CACHE_CONF" export --output "$PLZ_EXPORT_DIR" //src/core:core',
+ 'plz "$CACHE_CONF" --repo_root="$PLZ_EXPORT_DIR" build //src/core:core',
+ ]),
+ pre_cmd = 'PLZ_EXPORT_DIR="$(mktemp -d)"',
+)
+
+# Generic catch-all test on internal repo - notrim.
+plz_e2e_test(
+ name = "export_src_please_notrim_test",
+ cmd = " && ".join([
+ 'CACHE_CONF="--override=cache.dir:$(plz query reporoot)/plz-out/test-cache"',
+ 'plz "$CACHE_CONF" export --output "$PLZ_EXPORT_DIR" --notrim //src/core:core',
+ 'plz "$CACHE_CONF" --repo_root="$PLZ_EXPORT_DIR" build //src/core:core',
+ ]),
+ pre_cmd = 'PLZ_EXPORT_DIR="$(mktemp -d)"',
)
diff --git a/test/export/please_export_e2e_test.build_defs b/test/export/please_export_e2e_test.build_defs
index fec46eab7f..135d756612 100644
--- a/test/export/please_export_e2e_test.build_defs
+++ b/test/export/please_export_e2e_test.build_defs
@@ -40,7 +40,7 @@ def please_export_e2e_test(
if enforce_different_repos:
test_cmd += [
# Enforce source_repo and expected_repo differences
- f'(diff -rq "$DATA_SOURCE_REPO" "$DATA_EXPECTED_REPO" && \
+ f'(diff -rq "$DATA_SOURCE_REPO" "$DATA_EXPECTED_REPO" > /dev/null && \
echo "Source and Expected repos must differ" && exit 1 || true)',
]
@@ -50,7 +50,7 @@ def please_export_e2e_test(
# Golden-Master validation with expected repo. Done before any building to avoid plz-out
f'diff -ru "{exported_repo}" "$DATA_EXPECTED_REPO"',
# Tests building the exported repo which in turn ensures the sources are included
- f'plz --repo_root="{exported_repo}" build //...',
+ f'plz --repo_root="{exported_repo}" build //... > /dev/null',
] + [f'plz --repo_root="{exported_repo}" {cmd}' for cmd in cmd_on_export]
# Share the dir cache with the rest of the e2e tests; these repos build a Go toolchain and stdlib
diff --git a/test/export/test_builtins/expected_repo/BUILD_FILE b/test/export/test_builtins/expected_repo/BUILD_FILE
index a62a98a8d6..b840c2d9f1 100644
--- a/test/export/test_builtins/expected_repo/BUILD_FILE
+++ b/test/export/test_builtins/expected_repo/BUILD_FILE
@@ -2,5 +2,6 @@ genrule(
name = "native_genrule",
srcs = ["file.txt"],
outs = ["file.wordcount"],
- cmd = "wc $SRCS > $OUT",
+ cmd = "$TOOLS $SRCS > $OUT",
+ tools = ["//tools:tool"],
)
diff --git a/test/export/test_builtins/expected_repo/tools/BUILD_FILE b/test/export/test_builtins/expected_repo/tools/BUILD_FILE
new file mode 100644
index 0000000000..71d3129e2c
--- /dev/null
+++ b/test/export/test_builtins/expected_repo/tools/BUILD_FILE
@@ -0,0 +1,6 @@
+export_file(
+ name = "tool",
+ src = "tool.sh",
+ binary = True,
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_builtins/expected_repo/tools/tool.sh b/test/export/test_builtins/expected_repo/tools/tool.sh
new file mode 100644
index 0000000000..244a9b4ffd
--- /dev/null
+++ b/test/export/test_builtins/expected_repo/tools/tool.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+wc $@
diff --git a/test/export/test_builtins/source_repo/BUILD_FILE b/test/export/test_builtins/source_repo/BUILD_FILE
index db7732c487..a1a48a88a0 100644
--- a/test/export/test_builtins/source_repo/BUILD_FILE
+++ b/test/export/test_builtins/source_repo/BUILD_FILE
@@ -2,12 +2,13 @@ genrule(
name = "native_genrule",
srcs = ["file.txt"],
outs = ["file.wordcount"],
- cmd = "wc $SRCS > $OUT",
+ cmd = "$TOOLS $SRCS > $OUT",
+ tools = ["//tools:tool"],
)
genrule(
- name = "dummy_target_to_be_trimmed",
- srcs = ["dummy.txt"],
- outs = ["dummy"],
+ name = "unused_target_to_be_trimmed",
+ srcs = ["unused.txt"],
+ outs = ["unused"],
cmd = "cat $SRCS > $OUT",
)
diff --git a/test/export/test_builtins/source_repo/tools/BUILD_FILE b/test/export/test_builtins/source_repo/tools/BUILD_FILE
new file mode 100644
index 0000000000..71d3129e2c
--- /dev/null
+++ b/test/export/test_builtins/source_repo/tools/BUILD_FILE
@@ -0,0 +1,6 @@
+export_file(
+ name = "tool",
+ src = "tool.sh",
+ binary = True,
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_builtins/source_repo/tools/tool.sh b/test/export/test_builtins/source_repo/tools/tool.sh
new file mode 100644
index 0000000000..244a9b4ffd
--- /dev/null
+++ b/test/export/test_builtins/source_repo/tools/tool.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+wc $@
diff --git a/test/export/test_custom_def/source_repo/BUILD_FILE b/test/export/test_custom_def/source_repo/BUILD_FILE
index b0e288e983..31014faaf3 100644
--- a/test/export/test_custom_def/source_repo/BUILD_FILE
+++ b/test/export/test_custom_def/source_repo/BUILD_FILE
@@ -1,13 +1,21 @@
-subinclude("//build_defs:simple_build_def")
+subinclude(
+ "//build_defs:simple_build_def",
+ "//build_defs:unused_build_def",
+)
simple_custom_target(
- name = "simple_custom_target",
+ name = "unused",
srcs = ["file.txt"],
- outs = ["file_simple.out"],
+ outs = ["unused.out"],
+)
+
+unused_target(
+ name = "unused2",
+ outs = ["unused2.out"],
)
simple_custom_target(
- name = "dummy",
+ name = "simple_custom_target",
srcs = ["file.txt"],
- outs = ["dummy.out"],
+ outs = ["file_simple.out"],
)
diff --git a/test/export/test_custom_def/source_repo/build_defs/BUILD_FILE b/test/export/test_custom_def/source_repo/build_defs/BUILD_FILE
index 976d0dc22c..562e57a297 100644
--- a/test/export/test_custom_def/source_repo/build_defs/BUILD_FILE
+++ b/test/export/test_custom_def/source_repo/build_defs/BUILD_FILE
@@ -5,7 +5,7 @@ filegroup(
)
filegroup(
- name = "dummy_build_def",
- srcs = ["dummy.build_defs"],
+ name = "unused_build_def",
+ srcs = ["unused.build_defs"],
visibility = ["PUBLIC"],
)
diff --git a/test/export/test_custom_def/source_repo/build_defs/dummy.build_defs b/test/export/test_custom_def/source_repo/build_defs/unused.build_defs
similarity index 66%
rename from test/export/test_custom_def/source_repo/build_defs/dummy.build_defs
rename to test/export/test_custom_def/source_repo/build_defs/unused.build_defs
index 8cc3b6112a..88e56db977 100644
--- a/test/export/test_custom_def/source_repo/build_defs/dummy.build_defs
+++ b/test/export/test_custom_def/source_repo/build_defs/unused.build_defs
@@ -1,8 +1,8 @@
-def dummy_target(
+def unused_target(
name:str,
outs:list=[]):
return genrule(
name = name,
outs = outs,
- cmd = "echo dummy > $OUT",
+ cmd = "echo unused > $OUT",
)
diff --git a/test/export/test_custom_def_children/expected_repo/build_defs/BUILD_FILE b/test/export/test_custom_def_children/expected_repo/build_defs/BUILD_FILE
deleted file mode 100644
index e9b1bc8a20..0000000000
--- a/test/export/test_custom_def_children/expected_repo/build_defs/BUILD_FILE
+++ /dev/null
@@ -1,5 +0,0 @@
-filegroup(
- name = "custom_build_def",
- srcs = ["custom.build_defs"],
- visibility = ["PUBLIC"],
-)
diff --git a/test/export/test_custom_def_children/source_repo/build_defs/BUILD_FILE b/test/export/test_custom_def_children/source_repo/build_defs/BUILD_FILE
deleted file mode 100644
index e9b1bc8a20..0000000000
--- a/test/export/test_custom_def_children/source_repo/build_defs/BUILD_FILE
+++ /dev/null
@@ -1,5 +0,0 @@
-filegroup(
- name = "custom_build_def",
- srcs = ["custom.build_defs"],
- visibility = ["PUBLIC"],
-)
diff --git a/test/export/test_custom_def_children/BUILD b/test/export/test_custom_def_multiple_targets/BUILD
similarity index 87%
rename from test/export/test_custom_def_children/BUILD
rename to test/export/test_custom_def_multiple_targets/BUILD
index af8ef5b106..abd0536fd1 100644
--- a/test/export/test_custom_def_children/BUILD
+++ b/test/export/test_custom_def_multiple_targets/BUILD
@@ -5,7 +5,7 @@ please_export_e2e_test(
name = "export_custom_with_adjacent_target",
cmd_on_export = [
# Adjacent target of build def
- "build //:custom_target#adjacent",
+ "build //:custom_target_adjacent",
],
export_targets = ["//:custom_target"],
)
diff --git a/test/export/test_custom_def_children/expected_repo/.plzconfig b/test/export/test_custom_def_multiple_targets/expected_repo/.plzconfig
similarity index 100%
rename from test/export/test_custom_def_children/expected_repo/.plzconfig
rename to test/export/test_custom_def_multiple_targets/expected_repo/.plzconfig
diff --git a/test/export/test_custom_def_children/expected_repo/BUILD_FILE b/test/export/test_custom_def_multiple_targets/expected_repo/BUILD_FILE
similarity index 100%
rename from test/export/test_custom_def_children/expected_repo/BUILD_FILE
rename to test/export/test_custom_def_multiple_targets/expected_repo/BUILD_FILE
diff --git a/test/export/test_custom_def_multiple_targets/expected_repo/build_defs/BUILD_FILE b/test/export/test_custom_def_multiple_targets/expected_repo/build_defs/BUILD_FILE
new file mode 100644
index 0000000000..6e20067074
--- /dev/null
+++ b/test/export/test_custom_def_multiple_targets/expected_repo/build_defs/BUILD_FILE
@@ -0,0 +1,17 @@
+filegroup(
+ name = "custom_build_def",
+ srcs = ["custom.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "secondary_build_def",
+ srcs = ["secondary.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+export_file(
+ name = "secondary_file",
+ src = "secondary.file",
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_custom_def_children/source_repo/build_defs/custom.build_defs b/test/export/test_custom_def_multiple_targets/expected_repo/build_defs/custom.build_defs
similarity index 69%
rename from test/export/test_custom_def_children/source_repo/build_defs/custom.build_defs
rename to test/export/test_custom_def_multiple_targets/expected_repo/build_defs/custom.build_defs
index cc0e256e86..00bf12383e 100644
--- a/test/export/test_custom_def_children/source_repo/build_defs/custom.build_defs
+++ b/test/export/test_custom_def_multiple_targets/expected_repo/build_defs/custom.build_defs
@@ -1,13 +1,13 @@
+subinclude("//build_defs:secondary_build_def")
+
def custom_target(
name:str,
srcs:list=[],
outs:list=[],
outs_adjacent:list=[]):
- genrule(
- name = f"{name}#adjacent",
- srcs = srcs,
+ secondary_custom_target(
+ name = f"{name}_adjacent",
outs = outs_adjacent,
- cmd = "echo 'adjacent' > $OUT && cat $SRCS >> $OUT ",
)
return genrule(
name = name,
diff --git a/test/export/test_custom_def_multiple_targets/expected_repo/build_defs/secondary.build_defs b/test/export/test_custom_def_multiple_targets/expected_repo/build_defs/secondary.build_defs
new file mode 100644
index 0000000000..85b6470267
--- /dev/null
+++ b/test/export/test_custom_def_multiple_targets/expected_repo/build_defs/secondary.build_defs
@@ -0,0 +1,9 @@
+def secondary_custom_target(
+ name:str,
+ outs:list=[]):
+ return genrule(
+ name = name,
+ srcs = ["//build_defs:secondary_file"],
+ outs = outs,
+ cmd = "echo 'secondary' > $OUT && cat $SRCS >> $OUT ",
+ )
diff --git a/test/export/test_custom_def_multiple_targets/expected_repo/build_defs/secondary.file b/test/export/test_custom_def_multiple_targets/expected_repo/build_defs/secondary.file
new file mode 100644
index 0000000000..7a0bf8eb90
--- /dev/null
+++ b/test/export/test_custom_def_multiple_targets/expected_repo/build_defs/secondary.file
@@ -0,0 +1 @@
+File for testing
diff --git a/test/export/test_custom_def_children/expected_repo/file.txt b/test/export/test_custom_def_multiple_targets/expected_repo/file.txt
similarity index 100%
rename from test/export/test_custom_def_children/expected_repo/file.txt
rename to test/export/test_custom_def_multiple_targets/expected_repo/file.txt
diff --git a/test/export/test_custom_def_children/source_repo/.plzconfig b/test/export/test_custom_def_multiple_targets/source_repo/.plzconfig
similarity index 100%
rename from test/export/test_custom_def_children/source_repo/.plzconfig
rename to test/export/test_custom_def_multiple_targets/source_repo/.plzconfig
diff --git a/test/export/test_custom_def_children/source_repo/BUILD_FILE b/test/export/test_custom_def_multiple_targets/source_repo/BUILD_FILE
similarity index 70%
rename from test/export/test_custom_def_children/source_repo/BUILD_FILE
rename to test/export/test_custom_def_multiple_targets/source_repo/BUILD_FILE
index 9651c3d8fe..e91b06390a 100644
--- a/test/export/test_custom_def_children/source_repo/BUILD_FILE
+++ b/test/export/test_custom_def_multiple_targets/source_repo/BUILD_FILE
@@ -8,8 +8,8 @@ custom_target(
)
custom_target(
- name = "dummy",
+ name = "unused",
srcs = ["file.txt"],
- outs = ["dummy.out"],
- outs_adjacent = ["dummy_adjacent.out"],
+ outs = ["unused.out"],
+ outs_adjacent = ["unused_adjacent.out"],
)
diff --git a/test/export/test_custom_def_multiple_targets/source_repo/build_defs/BUILD_FILE b/test/export/test_custom_def_multiple_targets/source_repo/build_defs/BUILD_FILE
new file mode 100644
index 0000000000..6e20067074
--- /dev/null
+++ b/test/export/test_custom_def_multiple_targets/source_repo/build_defs/BUILD_FILE
@@ -0,0 +1,17 @@
+filegroup(
+ name = "custom_build_def",
+ srcs = ["custom.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "secondary_build_def",
+ srcs = ["secondary.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+export_file(
+ name = "secondary_file",
+ src = "secondary.file",
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_custom_def_children/expected_repo/build_defs/custom.build_defs b/test/export/test_custom_def_multiple_targets/source_repo/build_defs/custom.build_defs
similarity index 69%
rename from test/export/test_custom_def_children/expected_repo/build_defs/custom.build_defs
rename to test/export/test_custom_def_multiple_targets/source_repo/build_defs/custom.build_defs
index cc0e256e86..00bf12383e 100644
--- a/test/export/test_custom_def_children/expected_repo/build_defs/custom.build_defs
+++ b/test/export/test_custom_def_multiple_targets/source_repo/build_defs/custom.build_defs
@@ -1,13 +1,13 @@
+subinclude("//build_defs:secondary_build_def")
+
def custom_target(
name:str,
srcs:list=[],
outs:list=[],
outs_adjacent:list=[]):
- genrule(
- name = f"{name}#adjacent",
- srcs = srcs,
+ secondary_custom_target(
+ name = f"{name}_adjacent",
outs = outs_adjacent,
- cmd = "echo 'adjacent' > $OUT && cat $SRCS >> $OUT ",
)
return genrule(
name = name,
diff --git a/test/export/test_custom_def_multiple_targets/source_repo/build_defs/secondary.build_defs b/test/export/test_custom_def_multiple_targets/source_repo/build_defs/secondary.build_defs
new file mode 100644
index 0000000000..85b6470267
--- /dev/null
+++ b/test/export/test_custom_def_multiple_targets/source_repo/build_defs/secondary.build_defs
@@ -0,0 +1,9 @@
+def secondary_custom_target(
+ name:str,
+ outs:list=[]):
+ return genrule(
+ name = name,
+ srcs = ["//build_defs:secondary_file"],
+ outs = outs,
+ cmd = "echo 'secondary' > $OUT && cat $SRCS >> $OUT ",
+ )
diff --git a/test/export/test_custom_def_multiple_targets/source_repo/build_defs/secondary.file b/test/export/test_custom_def_multiple_targets/source_repo/build_defs/secondary.file
new file mode 100644
index 0000000000..7a0bf8eb90
--- /dev/null
+++ b/test/export/test_custom_def_multiple_targets/source_repo/build_defs/secondary.file
@@ -0,0 +1 @@
+File for testing
diff --git a/test/export/test_custom_def_children/source_repo/file.txt b/test/export/test_custom_def_multiple_targets/source_repo/file.txt
similarity index 100%
rename from test/export/test_custom_def_children/source_repo/file.txt
rename to test/export/test_custom_def_multiple_targets/source_repo/file.txt
diff --git a/test/export/test_deps/source_repo/BUILD_FILE b/test/export/test_deps/source_repo/BUILD_FILE
index 3c6766fc9e..53625b2c0c 100644
--- a/test/export/test_deps/source_repo/BUILD_FILE
+++ b/test/export/test_deps/source_repo/BUILD_FILE
@@ -17,8 +17,8 @@ genrule(
)
genrule(
- name = "dummy",
+ name = "unused",
srcs = ["file1.txt"],
- outs = ["dummy.out"],
+ outs = ["unused.out"],
cmd = "cat $SRCS > $OUT",
)
diff --git a/test/export/test_deps/source_repo/other_deps/BUILD_FILE b/test/export/test_deps/source_repo/other_deps/BUILD_FILE
index 8acaa9b9eb..6c1c541bfc 100644
--- a/test/export/test_deps/source_repo/other_deps/BUILD_FILE
+++ b/test/export/test_deps/source_repo/other_deps/BUILD_FILE
@@ -6,8 +6,8 @@ genrule(
)
genrule(
- name = "dummy",
- outs = ["dummy.out"],
- cmd = "echo 'dummy' > $OUT",
+ name = "unused",
+ outs = ["unused.out"],
+ cmd = "echo 'unused' > $OUT",
visibility = ["PUBLIC"],
)
diff --git a/test/export/test_dynamic_subinclude/BUILD b/test/export/test_dynamic_subinclude/BUILD
new file mode 100644
index 0000000000..b588cde421
--- /dev/null
+++ b/test/export/test_dynamic_subinclude/BUILD
@@ -0,0 +1,11 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+# Export targets generated by a custom build def in a loop with dynamic subincludes,
+# and validate that unused subincludes are trimmed.
+please_export_e2e_test(
+ name = "export_dynamic_subinclude",
+ export_targets = [
+ "//:a",
+ "//:b",
+ ],
+)
diff --git a/test/export/test_custom_in_file_def/expected_repo/.plzconfig b/test/export/test_dynamic_subinclude/expected_repo/.plzconfig
similarity index 100%
rename from test/export/test_custom_in_file_def/expected_repo/.plzconfig
rename to test/export/test_dynamic_subinclude/expected_repo/.plzconfig
diff --git a/test/export/test_dynamic_subinclude/expected_repo/BUILD_FILE b/test/export/test_dynamic_subinclude/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..31cf1e8c06
--- /dev/null
+++ b/test/export/test_dynamic_subinclude/expected_repo/BUILD_FILE
@@ -0,0 +1,7 @@
+subinclude("//build_defs:dynamic_build_def")
+
+for i in [
+ "a",
+ "b",
+]:
+ dynamic_target(name = i, def_name = f"{i}_build_def")
diff --git a/test/export/test_dynamic_subinclude/expected_repo/build_defs/BUILD_FILE b/test/export/test_dynamic_subinclude/expected_repo/build_defs/BUILD_FILE
new file mode 100644
index 0000000000..625da44b75
--- /dev/null
+++ b/test/export/test_dynamic_subinclude/expected_repo/build_defs/BUILD_FILE
@@ -0,0 +1,17 @@
+filegroup(
+ name = "dynamic_build_def",
+ srcs = ["dynamic.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "a_build_def",
+ srcs = ["a.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "b_build_def",
+ srcs = ["b.build_defs"],
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_dynamic_subinclude/expected_repo/build_defs/a.build_defs b/test/export/test_dynamic_subinclude/expected_repo/build_defs/a.build_defs
new file mode 100644
index 0000000000..f3ce9071c5
--- /dev/null
+++ b/test/export/test_dynamic_subinclude/expected_repo/build_defs/a.build_defs
@@ -0,0 +1,6 @@
+def helper_a(name:str):
+ genrule(
+ name = name,
+ outs = [f"{name}.out"],
+ cmd = f"echo {name} > $OUT",
+ )
diff --git a/test/export/test_dynamic_subinclude/expected_repo/build_defs/b.build_defs b/test/export/test_dynamic_subinclude/expected_repo/build_defs/b.build_defs
new file mode 100644
index 0000000000..03411fef71
--- /dev/null
+++ b/test/export/test_dynamic_subinclude/expected_repo/build_defs/b.build_defs
@@ -0,0 +1,6 @@
+def helper_b(name:str):
+ genrule(
+ name = name,
+ outs = [f"{name}.out"],
+ cmd = f"echo {name} > $OUT",
+ )
diff --git a/test/export/test_dynamic_subinclude/expected_repo/build_defs/dynamic.build_defs b/test/export/test_dynamic_subinclude/expected_repo/build_defs/dynamic.build_defs
new file mode 100644
index 0000000000..5337100c77
--- /dev/null
+++ b/test/export/test_dynamic_subinclude/expected_repo/build_defs/dynamic.build_defs
@@ -0,0 +1,6 @@
+def dynamic_target(name:str, def_name:str):
+ subinclude(f"//build_defs:{def_name}")
+ if name == "a":
+ helper_a(name = name)
+ elif name == "b":
+ helper_b(name = name)
diff --git a/test/export/test_custom_in_file_def/source_repo/.plzconfig b/test/export/test_dynamic_subinclude/source_repo/.plzconfig
similarity index 100%
rename from test/export/test_custom_in_file_def/source_repo/.plzconfig
rename to test/export/test_dynamic_subinclude/source_repo/.plzconfig
diff --git a/test/export/test_dynamic_subinclude/source_repo/BUILD_FILE b/test/export/test_dynamic_subinclude/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..31cf1e8c06
--- /dev/null
+++ b/test/export/test_dynamic_subinclude/source_repo/BUILD_FILE
@@ -0,0 +1,7 @@
+subinclude("//build_defs:dynamic_build_def")
+
+for i in [
+ "a",
+ "b",
+]:
+ dynamic_target(name = i, def_name = f"{i}_build_def")
diff --git a/test/export/test_dynamic_subinclude/source_repo/build_defs/BUILD_FILE b/test/export/test_dynamic_subinclude/source_repo/build_defs/BUILD_FILE
new file mode 100644
index 0000000000..bb0ab1aaec
--- /dev/null
+++ b/test/export/test_dynamic_subinclude/source_repo/build_defs/BUILD_FILE
@@ -0,0 +1,23 @@
+filegroup(
+ name = "dynamic_build_def",
+ srcs = ["dynamic.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "a_build_def",
+ srcs = ["a.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "b_build_def",
+ srcs = ["b.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "unused_build_def",
+ srcs = ["unused.build_defs"],
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_dynamic_subinclude/source_repo/build_defs/a.build_defs b/test/export/test_dynamic_subinclude/source_repo/build_defs/a.build_defs
new file mode 100644
index 0000000000..f3ce9071c5
--- /dev/null
+++ b/test/export/test_dynamic_subinclude/source_repo/build_defs/a.build_defs
@@ -0,0 +1,6 @@
+def helper_a(name:str):
+ genrule(
+ name = name,
+ outs = [f"{name}.out"],
+ cmd = f"echo {name} > $OUT",
+ )
diff --git a/test/export/test_dynamic_subinclude/source_repo/build_defs/b.build_defs b/test/export/test_dynamic_subinclude/source_repo/build_defs/b.build_defs
new file mode 100644
index 0000000000..03411fef71
--- /dev/null
+++ b/test/export/test_dynamic_subinclude/source_repo/build_defs/b.build_defs
@@ -0,0 +1,6 @@
+def helper_b(name:str):
+ genrule(
+ name = name,
+ outs = [f"{name}.out"],
+ cmd = f"echo {name} > $OUT",
+ )
diff --git a/test/export/test_dynamic_subinclude/source_repo/build_defs/dynamic.build_defs b/test/export/test_dynamic_subinclude/source_repo/build_defs/dynamic.build_defs
new file mode 100644
index 0000000000..5337100c77
--- /dev/null
+++ b/test/export/test_dynamic_subinclude/source_repo/build_defs/dynamic.build_defs
@@ -0,0 +1,6 @@
+def dynamic_target(name:str, def_name:str):
+ subinclude(f"//build_defs:{def_name}")
+ if name == "a":
+ helper_a(name = name)
+ elif name == "b":
+ helper_b(name = name)
diff --git a/test/export/test_dynamic_subinclude/source_repo/build_defs/unused.build_defs b/test/export/test_dynamic_subinclude/source_repo/build_defs/unused.build_defs
new file mode 100644
index 0000000000..afaae2a8ba
--- /dev/null
+++ b/test/export/test_dynamic_subinclude/source_repo/build_defs/unused.build_defs
@@ -0,0 +1,2 @@
+def unused_helper(name:str):
+ pass
diff --git a/test/export/test_elif/BUILD b/test/export/test_elif/BUILD
new file mode 100644
index 0000000000..263b96626e
--- /dev/null
+++ b/test/export/test_elif/BUILD
@@ -0,0 +1,10 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+# This test validates the trimming of if/elif/else blocks during export.
+# Specifically, it tests that when exporting a target defined inside an 'elif' block,
+# the other unused 'if' and 'else' blocks are correctly trimmed and replaced with 'pass'.
+please_export_e2e_test(
+ name = "export_elif",
+ enforce_different_repos = False,
+ export_targets = ["//:b"],
+)
diff --git a/test/export/test_elif/expected_repo/.plzconfig b/test/export/test_elif/expected_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_elif/expected_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_elif/expected_repo/BUILD_FILE b/test/export/test_elif/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..d550227d92
--- /dev/null
+++ b/test/export/test_elif/expected_repo/BUILD_FILE
@@ -0,0 +1,10 @@
+if False:
+ pass # Trimmed during export
+elif True:
+ genrule(
+ name = "b",
+ outs = ["b.txt"],
+ cmd = "echo b > $OUT",
+ )
+else:
+ pass # Trimmed during export
diff --git a/test/export/test_elif/source_repo/.plzconfig b/test/export/test_elif/source_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_elif/source_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_elif/source_repo/BUILD_FILE b/test/export/test_elif/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..e360e4bdb3
--- /dev/null
+++ b/test/export/test_elif/source_repo/BUILD_FILE
@@ -0,0 +1,18 @@
+if False:
+ genrule(
+ name = "a",
+ outs = ["a.txt"],
+ cmd = "echo a > $OUT",
+ )
+elif True:
+ genrule(
+ name = "b",
+ outs = ["b.txt"],
+ cmd = "echo b > $OUT",
+ )
+else:
+ genrule(
+ name = "c",
+ outs = ["c.txt"],
+ cmd = "echo c > $OUT",
+ )
diff --git a/test/export/test_for/BUILD b/test/export/test_for/BUILD
new file mode 100644
index 0000000000..1b4bb42859
--- /dev/null
+++ b/test/export/test_for/BUILD
@@ -0,0 +1,6 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+please_export_e2e_test(
+ name = "export_for",
+ export_targets = ["//:a"],
+)
diff --git a/test/export/test_for/expected_repo/.plzconfig b/test/export/test_for/expected_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_for/expected_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_for/expected_repo/BUILD_FILE b/test/export/test_for/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..5754be2e47
--- /dev/null
+++ b/test/export/test_for/expected_repo/BUILD_FILE
@@ -0,0 +1,11 @@
+for i in [
+ "a",
+ "b",
+ "c",
+]:
+ genrule(
+ name = i,
+ srcs = [f"{i}.src"],
+ outs = [f"{i}.txt"],
+ cmd = f"cat $SRCS > $OUT",
+ )
diff --git a/test/export/test_for/expected_repo/a.src b/test/export/test_for/expected_repo/a.src
new file mode 100644
index 0000000000..7898192261
--- /dev/null
+++ b/test/export/test_for/expected_repo/a.src
@@ -0,0 +1 @@
+a
diff --git a/test/export/test_for/expected_repo/b.src b/test/export/test_for/expected_repo/b.src
new file mode 100644
index 0000000000..6178079822
--- /dev/null
+++ b/test/export/test_for/expected_repo/b.src
@@ -0,0 +1 @@
+b
diff --git a/test/export/test_for/expected_repo/c.src b/test/export/test_for/expected_repo/c.src
new file mode 100644
index 0000000000..f2ad6c76f0
--- /dev/null
+++ b/test/export/test_for/expected_repo/c.src
@@ -0,0 +1 @@
+c
diff --git a/test/export/test_for/source_repo/.plzconfig b/test/export/test_for/source_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_for/source_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_for/source_repo/BUILD_FILE b/test/export/test_for/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..50d3a3ca34
--- /dev/null
+++ b/test/export/test_for/source_repo/BUILD_FILE
@@ -0,0 +1,17 @@
+for i in [
+ "a",
+ "b",
+ "c",
+]:
+ genrule(
+ name = i,
+ srcs = [f"{i}.src"],
+ outs = [f"{i}.txt"],
+ cmd = f"cat $SRCS > $OUT",
+ )
+
+genrule(
+ name = "d",
+ outs = ["d.txt"],
+ cmd = "echo d > $OUT",
+)
diff --git a/test/export/test_for/source_repo/a.src b/test/export/test_for/source_repo/a.src
new file mode 100644
index 0000000000..7898192261
--- /dev/null
+++ b/test/export/test_for/source_repo/a.src
@@ -0,0 +1 @@
+a
diff --git a/test/export/test_for/source_repo/b.src b/test/export/test_for/source_repo/b.src
new file mode 100644
index 0000000000..6178079822
--- /dev/null
+++ b/test/export/test_for/source_repo/b.src
@@ -0,0 +1 @@
+b
diff --git a/test/export/test_for/source_repo/c.src b/test/export/test_for/source_repo/c.src
new file mode 100644
index 0000000000..f2ad6c76f0
--- /dev/null
+++ b/test/export/test_for/source_repo/c.src
@@ -0,0 +1 @@
+c
diff --git a/test/export/test_for_glob/BUILD b/test/export/test_for_glob/BUILD
new file mode 100644
index 0000000000..3733c75062
--- /dev/null
+++ b/test/export/test_for_glob/BUILD
@@ -0,0 +1,6 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+please_export_e2e_test(
+ name = "export_for_glob",
+ export_targets = ["//pkg:all_files"],
+)
diff --git a/test/export/test_for_glob/expected_repo/.plzconfig b/test/export/test_for_glob/expected_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_for_glob/expected_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_for_glob/expected_repo/pkg/BUILD_FILE b/test/export/test_for_glob/expected_repo/pkg/BUILD_FILE
new file mode 100644
index 0000000000..f4965461be
--- /dev/null
+++ b/test/export/test_for_glob/expected_repo/pkg/BUILD_FILE
@@ -0,0 +1,17 @@
+for file in glob(["file*.in"]):
+ genrule(
+ name = "target_" + file.removesuffix(".in"),
+ srcs = [file],
+ outs = [file.removesuffix(".in") + ".out"],
+ cmd = "cp $SRCS $OUT",
+ )
+
+filegroup(
+ name = "all_files",
+ srcs = [
+ ":target_file1",
+ ],
+)
+
+for file in glob(["doc*.in"]):
+ pass # Trimmed during export
diff --git a/test/export/test_for_glob/expected_repo/pkg/doc1.in b/test/export/test_for_glob/expected_repo/pkg/doc1.in
new file mode 100644
index 0000000000..516682f61d
--- /dev/null
+++ b/test/export/test_for_glob/expected_repo/pkg/doc1.in
@@ -0,0 +1 @@
+doc1
diff --git a/test/export/test_for_glob/expected_repo/pkg/doc2.in b/test/export/test_for_glob/expected_repo/pkg/doc2.in
new file mode 100644
index 0000000000..67f7bbeeda
--- /dev/null
+++ b/test/export/test_for_glob/expected_repo/pkg/doc2.in
@@ -0,0 +1 @@
+doc2
diff --git a/test/export/test_for_glob/expected_repo/pkg/file1.in b/test/export/test_for_glob/expected_repo/pkg/file1.in
new file mode 100644
index 0000000000..d9039017ab
--- /dev/null
+++ b/test/export/test_for_glob/expected_repo/pkg/file1.in
@@ -0,0 +1 @@
+file1 content
diff --git a/test/export/test_for_glob/expected_repo/pkg/file2.in b/test/export/test_for_glob/expected_repo/pkg/file2.in
new file mode 100644
index 0000000000..f3c77b12c6
--- /dev/null
+++ b/test/export/test_for_glob/expected_repo/pkg/file2.in
@@ -0,0 +1 @@
+file2 content
diff --git a/test/export/test_for_glob/source_repo/.plzconfig b/test/export/test_for_glob/source_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_for_glob/source_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_for_glob/source_repo/pkg/BUILD_FILE b/test/export/test_for_glob/source_repo/pkg/BUILD_FILE
new file mode 100644
index 0000000000..4af14838dd
--- /dev/null
+++ b/test/export/test_for_glob/source_repo/pkg/BUILD_FILE
@@ -0,0 +1,29 @@
+for file in glob(["file*.in"]):
+ genrule(
+ name = "target_" + file.removesuffix(".in"),
+ srcs = [file],
+ outs = [file.removesuffix(".in") + ".out"],
+ cmd = "cp $SRCS $OUT",
+ )
+
+genrule(
+ name = "unused",
+ srcs = glob(["unused*.in"]),
+ outs = ["unused.out"],
+ cmd = "cp $SRCS $OUT",
+)
+
+filegroup(
+ name = "all_files",
+ srcs = [
+ ":target_file1",
+ ],
+)
+
+for file in glob(["doc*.in"]):
+ genrule(
+ name = "target_" + file.removesuffix(".in"),
+ srcs = [file],
+ outs = [file.removesuffix(".in") + ".out"],
+ cmd = "cp $SRCS $OUT",
+ )
diff --git a/test/export/test_for_glob/source_repo/pkg/doc1.in b/test/export/test_for_glob/source_repo/pkg/doc1.in
new file mode 100644
index 0000000000..516682f61d
--- /dev/null
+++ b/test/export/test_for_glob/source_repo/pkg/doc1.in
@@ -0,0 +1 @@
+doc1
diff --git a/test/export/test_for_glob/source_repo/pkg/doc2.in b/test/export/test_for_glob/source_repo/pkg/doc2.in
new file mode 100644
index 0000000000..67f7bbeeda
--- /dev/null
+++ b/test/export/test_for_glob/source_repo/pkg/doc2.in
@@ -0,0 +1 @@
+doc2
diff --git a/test/export/test_for_glob/source_repo/pkg/file1.in b/test/export/test_for_glob/source_repo/pkg/file1.in
new file mode 100644
index 0000000000..d9039017ab
--- /dev/null
+++ b/test/export/test_for_glob/source_repo/pkg/file1.in
@@ -0,0 +1 @@
+file1 content
diff --git a/test/export/test_for_glob/source_repo/pkg/file2.in b/test/export/test_for_glob/source_repo/pkg/file2.in
new file mode 100644
index 0000000000..f3c77b12c6
--- /dev/null
+++ b/test/export/test_for_glob/source_repo/pkg/file2.in
@@ -0,0 +1 @@
+file2 content
diff --git a/test/export/test_for_glob/source_repo/pkg/unused.in b/test/export/test_for_glob/source_repo/pkg/unused.in
new file mode 100644
index 0000000000..14af70badd
--- /dev/null
+++ b/test/export/test_for_glob/source_repo/pkg/unused.in
@@ -0,0 +1 @@
+unused content
diff --git a/test/export/test_for_if/BUILD b/test/export/test_for_if/BUILD
new file mode 100644
index 0000000000..75b06a3508
--- /dev/null
+++ b/test/export/test_for_if/BUILD
@@ -0,0 +1,6 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+please_export_e2e_test(
+ name = "export_for_if",
+ export_targets = ["//:a"],
+)
diff --git a/test/export/test_for_if/expected_repo/.plzconfig b/test/export/test_for_if/expected_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_for_if/expected_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_for_if/expected_repo/BUILD_FILE b/test/export/test_for_if/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..f27eaa34ac
--- /dev/null
+++ b/test/export/test_for_if/expected_repo/BUILD_FILE
@@ -0,0 +1,13 @@
+for i in [
+ "a",
+ "b",
+]:
+ if i == "a":
+ genrule(
+ name = "a",
+ srcs = ["a.src"],
+ outs = ["a.txt"],
+ cmd = "cat $SRCS > $OUT",
+ )
+ elif i == "b":
+ pass # Trimmed during export
diff --git a/test/export/test_for_if/expected_repo/a.src b/test/export/test_for_if/expected_repo/a.src
new file mode 100644
index 0000000000..7898192261
--- /dev/null
+++ b/test/export/test_for_if/expected_repo/a.src
@@ -0,0 +1 @@
+a
diff --git a/test/export/test_for_if/source_repo/.plzconfig b/test/export/test_for_if/source_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_for_if/source_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_for_if/source_repo/BUILD_FILE b/test/export/test_for_if/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..eaf15da2bf
--- /dev/null
+++ b/test/export/test_for_if/source_repo/BUILD_FILE
@@ -0,0 +1,18 @@
+for i in [
+ "a",
+ "b",
+]:
+ if i == "a":
+ genrule(
+ name = "a",
+ srcs = ["a.src"],
+ outs = ["a.txt"],
+ cmd = "cat $SRCS > $OUT",
+ )
+ elif i == "b":
+ genrule(
+ name = "b",
+ srcs = ["b.src"],
+ outs = ["b.txt"],
+ cmd = "cat $SRCS > $OUT",
+ )
diff --git a/test/export/test_for_if/source_repo/a.src b/test/export/test_for_if/source_repo/a.src
new file mode 100644
index 0000000000..7898192261
--- /dev/null
+++ b/test/export/test_for_if/source_repo/a.src
@@ -0,0 +1 @@
+a
diff --git a/test/export/test_for_if/source_repo/b.src b/test/export/test_for_if/source_repo/b.src
new file mode 100644
index 0000000000..6178079822
--- /dev/null
+++ b/test/export/test_for_if/source_repo/b.src
@@ -0,0 +1 @@
+b
diff --git a/test/export/test_for_if_both/BUILD b/test/export/test_for_if_both/BUILD
new file mode 100644
index 0000000000..12734eadef
--- /dev/null
+++ b/test/export/test_for_if_both/BUILD
@@ -0,0 +1,10 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+# Export both statements inside the inner if
+please_export_e2e_test(
+ name = "export_for_if_both",
+ export_targets = [
+ "//:a",
+ "//:b",
+ ],
+)
diff --git a/test/export/test_for_if_both/expected_repo/.plzconfig b/test/export/test_for_if_both/expected_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_for_if_both/expected_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_for_if_both/expected_repo/BUILD_FILE b/test/export/test_for_if_both/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..73218736ba
--- /dev/null
+++ b/test/export/test_for_if_both/expected_repo/BUILD_FILE
@@ -0,0 +1,20 @@
+for i in [
+ "a",
+ "b",
+]:
+ if i == "a":
+ genrule(
+ name = "a",
+ srcs = ["a.src"],
+ outs = ["a.txt"],
+ cmd = "cat $SRCS > $OUT",
+ )
+ elif i == "b":
+ genrule(
+ name = "b",
+ srcs = ["b.src"],
+ outs = ["b.txt"],
+ cmd = "cat $SRCS > $OUT",
+ )
+ else:
+ pass # Trimmed during export
diff --git a/test/export/test_for_if_both/expected_repo/a.src b/test/export/test_for_if_both/expected_repo/a.src
new file mode 100644
index 0000000000..7898192261
--- /dev/null
+++ b/test/export/test_for_if_both/expected_repo/a.src
@@ -0,0 +1 @@
+a
diff --git a/test/export/test_for_if_both/expected_repo/b.src b/test/export/test_for_if_both/expected_repo/b.src
new file mode 100644
index 0000000000..6178079822
--- /dev/null
+++ b/test/export/test_for_if_both/expected_repo/b.src
@@ -0,0 +1 @@
+b
diff --git a/test/export/test_for_if_both/source_repo/.plzconfig b/test/export/test_for_if_both/source_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_for_if_both/source_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_for_if_both/source_repo/BUILD_FILE b/test/export/test_for_if_both/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..66abd06cb7
--- /dev/null
+++ b/test/export/test_for_if_both/source_repo/BUILD_FILE
@@ -0,0 +1,25 @@
+for i in [
+ "a",
+ "b",
+]:
+ if i == "a":
+ genrule(
+ name = "a",
+ srcs = ["a.src"],
+ outs = ["a.txt"],
+ cmd = "cat $SRCS > $OUT",
+ )
+ elif i == "b":
+ genrule(
+ name = "b",
+ srcs = ["b.src"],
+ outs = ["b.txt"],
+ cmd = "cat $SRCS > $OUT",
+ )
+ else:
+ genrule(
+ name = "c",
+ srcs = ["c.src"],
+ outs = ["c.txt"],
+ cmd = "cat $SRCS > $OUT",
+ )
diff --git a/test/export/test_for_if_both/source_repo/a.src b/test/export/test_for_if_both/source_repo/a.src
new file mode 100644
index 0000000000..7898192261
--- /dev/null
+++ b/test/export/test_for_if_both/source_repo/a.src
@@ -0,0 +1 @@
+a
diff --git a/test/export/test_for_if_both/source_repo/b.src b/test/export/test_for_if_both/source_repo/b.src
new file mode 100644
index 0000000000..6178079822
--- /dev/null
+++ b/test/export/test_for_if_both/source_repo/b.src
@@ -0,0 +1 @@
+b
diff --git a/test/export/test_for_if_both/source_repo/c.src b/test/export/test_for_if_both/source_repo/c.src
new file mode 100644
index 0000000000..f2ad6c76f0
--- /dev/null
+++ b/test/export/test_for_if_both/source_repo/c.src
@@ -0,0 +1 @@
+c
diff --git a/test/export/test_glob/BUILD b/test/export/test_glob/BUILD
new file mode 100644
index 0000000000..5706c6b860
--- /dev/null
+++ b/test/export/test_glob/BUILD
@@ -0,0 +1,7 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+# Export a target that uses glob to select inputs.
+please_export_e2e_test(
+ name = "export_glob",
+ export_targets = ["//:glob_target"],
+)
diff --git a/test/export/test_glob/expected_repo/.plzconfig b/test/export/test_glob/expected_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_glob/expected_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_glob/expected_repo/BUILD_FILE b/test/export/test_glob/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..b06542f61e
--- /dev/null
+++ b/test/export/test_glob/expected_repo/BUILD_FILE
@@ -0,0 +1,9 @@
+genrule(
+ name = "glob_target",
+ srcs = glob(
+ ["*.txt"],
+ exclude = ["ignored.txt"],
+ ),
+ outs = ["out.txt"],
+ cmd = "cat $SRCS > $OUT",
+)
diff --git a/test/export/test_glob/expected_repo/file1.txt b/test/export/test_glob/expected_repo/file1.txt
new file mode 100644
index 0000000000..b5cea514f6
--- /dev/null
+++ b/test/export/test_glob/expected_repo/file1.txt
@@ -0,0 +1 @@
+hello from file 1
diff --git a/test/export/test_glob/expected_repo/file2.txt b/test/export/test_glob/expected_repo/file2.txt
new file mode 100644
index 0000000000..7331ba8736
--- /dev/null
+++ b/test/export/test_glob/expected_repo/file2.txt
@@ -0,0 +1 @@
+hello from file 2
diff --git a/test/export/test_glob/source_repo/.plzconfig b/test/export/test_glob/source_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_glob/source_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_glob/source_repo/BUILD_FILE b/test/export/test_glob/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..6a80b7817e
--- /dev/null
+++ b/test/export/test_glob/source_repo/BUILD_FILE
@@ -0,0 +1,16 @@
+genrule(
+ name = "glob_target",
+ srcs = glob(
+ ["*.txt"],
+ exclude = ["ignored.txt"],
+ ),
+ outs = ["out.txt"],
+ cmd = "cat $SRCS > $OUT",
+)
+
+genrule(
+ name = "unused",
+ srcs = ["unused.dat"],
+ outs = ["unused.out"],
+ cmd = "cat $SRCS > $OUT",
+)
diff --git a/test/export/test_glob/source_repo/file1.txt b/test/export/test_glob/source_repo/file1.txt
new file mode 100644
index 0000000000..b5cea514f6
--- /dev/null
+++ b/test/export/test_glob/source_repo/file1.txt
@@ -0,0 +1 @@
+hello from file 1
diff --git a/test/export/test_glob/source_repo/file2.txt b/test/export/test_glob/source_repo/file2.txt
new file mode 100644
index 0000000000..7331ba8736
--- /dev/null
+++ b/test/export/test_glob/source_repo/file2.txt
@@ -0,0 +1 @@
+hello from file 2
diff --git a/test/export/test_glob/source_repo/ignored.txt b/test/export/test_glob/source_repo/ignored.txt
new file mode 100644
index 0000000000..44d282adba
--- /dev/null
+++ b/test/export/test_glob/source_repo/ignored.txt
@@ -0,0 +1 @@
+hello from ignored file
diff --git a/test/export/test_glob/source_repo/unused.dat b/test/export/test_glob/source_repo/unused.dat
new file mode 100644
index 0000000000..e8f53910e2
--- /dev/null
+++ b/test/export/test_glob/source_repo/unused.dat
@@ -0,0 +1 @@
+hello from unused file
diff --git a/test/export/test_go_bin/expected_repo/BUILD_FILE b/test/export/test_go_bin/expected_repo/BUILD_FILE
index d0d1a145c0..015bd671a5 100644
--- a/test/export/test_go_bin/expected_repo/BUILD_FILE
+++ b/test/export/test_go_bin/expected_repo/BUILD_FILE
@@ -4,6 +4,6 @@ go_binary(
name = "bin_go_dep",
srcs = ["main.go"],
deps = [
- "///third_party/go/github.com_google_go-cmp//cmp",
+ "//third_party/go:cmp",
],
)
diff --git a/test/export/test_go_bin/expected_repo/third_party/go/BUILD_FILE b/test/export/test_go_bin/expected_repo/third_party/go/BUILD_FILE
index 652eab6b9b..9f9ec3ffdd 100644
--- a/test/export/test_go_bin/expected_repo/third_party/go/BUILD_FILE
+++ b/test/export/test_go_bin/expected_repo/third_party/go/BUILD_FILE
@@ -13,6 +13,8 @@ go_stdlib(
)
go_repo(
+ name = "cmp",
+ install = ["..."],
module = "github.com/google/go-cmp",
version = "v0.5.6",
)
diff --git a/test/export/test_go_bin/source_repo/BUILD_FILE b/test/export/test_go_bin/source_repo/BUILD_FILE
index d37abc25b4..c383de466b 100644
--- a/test/export/test_go_bin/source_repo/BUILD_FILE
+++ b/test/export/test_go_bin/source_repo/BUILD_FILE
@@ -4,14 +4,14 @@ go_binary(
name = "bin_go_dep",
srcs = ["main.go"],
deps = [
- "///third_party/go/github.com_google_go-cmp//cmp",
+ "//third_party/go:cmp",
],
)
go_binary(
- name = "dummy",
- srcs = ["dummy.go"],
+ name = "unused",
+ srcs = ["unused.go"],
deps = [
- "///third_party/go/github.com_stretchr_testify//:testify",
+ "//third_party/go:uuid",
],
)
diff --git a/test/export/test_go_bin/source_repo/dummy.go b/test/export/test_go_bin/source_repo/dummy.go
deleted file mode 100644
index 3c49d61525..0000000000
--- a/test/export/test_go_bin/source_repo/dummy.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package dummy
-
-import (
- "fmt"
-
- "github.com/stretchr/testify/"
-)
-
-func main() {
- return
-}
diff --git a/test/export/test_go_bin/source_repo/third_party/go/BUILD_FILE b/test/export/test_go_bin/source_repo/third_party/go/BUILD_FILE
index e8eb1d28e9..6f9e68af5e 100644
--- a/test/export/test_go_bin/source_repo/third_party/go/BUILD_FILE
+++ b/test/export/test_go_bin/source_repo/third_party/go/BUILD_FILE
@@ -13,25 +13,15 @@ go_stdlib(
)
go_repo(
+ name = "cmp",
+ install = ["..."],
module = "github.com/google/go-cmp",
version = "v0.5.6",
)
go_repo(
- # Dummy, unused target
- name = "testify",
- install = ["..."],
- licences = ["MIT"],
- module = "github.com/stretchr/testify",
- version = "v1.7.0",
- deps = [":yaml.v3"], # test we can depend on go_modules
-)
-
-go_module(
- # Dummy, unused target
- name = "yaml.v3",
- licences = ["MIT"],
- module = "gopkg.in/yaml.v3",
- version = "v3.0.0-20210107192922-496545a6307b",
- visibility = ["PUBLIC"],
+ # unused, unused target
+ name = "uuid",
+ module = "github.com/google/uuid",
+ version = "v1.6.0",
)
diff --git a/test/export/test_go_bin/source_repo/unused.go b/test/export/test_go_bin/source_repo/unused.go
new file mode 100644
index 0000000000..77e0b3d278
--- /dev/null
+++ b/test/export/test_go_bin/source_repo/unused.go
@@ -0,0 +1,5 @@
+package unused
+
+func main() {
+ return
+}
diff --git a/test/export/test_go_test/BUILD b/test/export/test_go_test/BUILD
new file mode 100644
index 0000000000..e8c41a89e9
--- /dev/null
+++ b/test/export/test_go_test/BUILD
@@ -0,0 +1,7 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+# Test go_test target with a go third_party dependency that implicitly requires other packages.
+please_export_e2e_test(
+ name = "export_go_test",
+ export_targets = ["//:test"],
+)
diff --git a/test/export/test_go_test/expected_repo/.plzconfig b/test/export/test_go_test/expected_repo/.plzconfig
new file mode 100644
index 0000000000..7fb433a97e
--- /dev/null
+++ b/test/export/test_go_test/expected_repo/.plzconfig
@@ -0,0 +1,10 @@
+[Parse]
+BuildFileName = BUILD # required by subrepos
+BuildFileName = BUILD_FILE
+preloadsubincludes = ///go//build_defs:go
+
+[Plugin "go"]
+Target = //plugins:go
+GoTool = //third_party/go:toolchain|go
+STDLib = //third_party/go:std
+ModFile = //:go_mod
diff --git a/test/export/test_go_test/expected_repo/BUILD_FILE b/test/export/test_go_test/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..516ef528ca
--- /dev/null
+++ b/test/export/test_go_test/expected_repo/BUILD_FILE
@@ -0,0 +1,11 @@
+filegroup(
+ name = "go_mod",
+ srcs = ["go.mod"],
+ visibility = ["PUBLIC"],
+)
+
+go_test(
+ name = "test",
+ srcs = ["test.go"],
+ deps = ["///third_party/go/github.com_stretchr_testify//assert"],
+)
diff --git a/test/export/test_go_test/expected_repo/go.mod b/test/export/test_go_test/expected_repo/go.mod
new file mode 100644
index 0000000000..7fcf309969
--- /dev/null
+++ b/test/export/test_go_test/expected_repo/go.mod
@@ -0,0 +1,11 @@
+module github.com/thought-machine/please/test_repo
+
+go 1.23.0
+
+require github.com/stretchr/testify v1.9.0
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/test/export/test_go_test/expected_repo/plugins/BUILD_FILE b/test/export/test_go_test/expected_repo/plugins/BUILD_FILE
new file mode 100644
index 0000000000..55e81b8e1d
--- /dev/null
+++ b/test/export/test_go_test/expected_repo/plugins/BUILD_FILE
@@ -0,0 +1,6 @@
+plugin_repo(
+ name = "go",
+ owner = "please-build",
+ plugin = "go-rules",
+ revision = "v1.30.0",
+)
diff --git a/test/export/test_go_test/expected_repo/test.go b/test/export/test_go_test/expected_repo/test.go
new file mode 100644
index 0000000000..aaf3c0c44b
--- /dev/null
+++ b/test/export/test_go_test/expected_repo/test.go
@@ -0,0 +1,11 @@
+package test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSomething(t *testing.T) {
+ assert.NotEqual(t, 0, 1)
+}
diff --git a/test/export/test_go_test/expected_repo/third_party/go/BUILD_FILE b/test/export/test_go_test/expected_repo/third_party/go/BUILD_FILE
new file mode 100644
index 0000000000..42a26f4f9c
--- /dev/null
+++ b/test/export/test_go_test/expected_repo/third_party/go/BUILD_FILE
@@ -0,0 +1,30 @@
+go_toolchain(
+ name = "toolchain",
+ version = "1.26.2",
+)
+
+go_stdlib(name = "std")
+
+go_repo(
+ licences = ["MIT"],
+ module = "github.com/stretchr/testify",
+ version = "v1.9.0",
+)
+
+go_repo(
+ licences = ["ISC"],
+ module = "github.com/davecgh/go-spew",
+ version = "v1.1.1",
+)
+
+go_repo(
+ licences = ["BSD-3-Clause"],
+ module = "github.com/pmezard/go-difflib",
+ version = "v1.0.0",
+)
+
+go_repo(
+ licences = ["MIT"],
+ module = "gopkg.in/yaml.v3",
+ version = "v3.0.1",
+)
diff --git a/test/export/test_go_test/source_repo/.plzconfig b/test/export/test_go_test/source_repo/.plzconfig
new file mode 100644
index 0000000000..7fb433a97e
--- /dev/null
+++ b/test/export/test_go_test/source_repo/.plzconfig
@@ -0,0 +1,10 @@
+[Parse]
+BuildFileName = BUILD # required by subrepos
+BuildFileName = BUILD_FILE
+preloadsubincludes = ///go//build_defs:go
+
+[Plugin "go"]
+Target = //plugins:go
+GoTool = //third_party/go:toolchain|go
+STDLib = //third_party/go:std
+ModFile = //:go_mod
diff --git a/test/export/test_go_test/source_repo/BUILD_FILE b/test/export/test_go_test/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..e7f8462620
--- /dev/null
+++ b/test/export/test_go_test/source_repo/BUILD_FILE
@@ -0,0 +1,19 @@
+filegroup(
+ name = "go_mod",
+ srcs = ["go.mod"],
+ visibility = ["PUBLIC"],
+)
+
+go_test(
+ name = "test",
+ srcs = ["test.go"],
+ deps = ["///third_party/go/github.com_stretchr_testify//assert"],
+)
+
+go_binary(
+ name = "unused",
+ srcs = ["main.go"],
+ deps = [
+ "///third_party/go/github.com_google_go-cmp//cmp:cmp",
+ ],
+)
diff --git a/test/export/test_go_test/source_repo/go.mod b/test/export/test_go_test/source_repo/go.mod
new file mode 100644
index 0000000000..7fcf309969
--- /dev/null
+++ b/test/export/test_go_test/source_repo/go.mod
@@ -0,0 +1,11 @@
+module github.com/thought-machine/please/test_repo
+
+go 1.23.0
+
+require github.com/stretchr/testify v1.9.0
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/test/export/test_go_test/source_repo/main.go b/test/export/test_go_test/source_repo/main.go
new file mode 100644
index 0000000000..0853120a28
--- /dev/null
+++ b/test/export/test_go_test/source_repo/main.go
@@ -0,0 +1,11 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func main() {
+ fmt.Print(cmp.Equal(1, 1))
+}
diff --git a/test/export/test_go_test/source_repo/plugins/BUILD_FILE b/test/export/test_go_test/source_repo/plugins/BUILD_FILE
new file mode 100644
index 0000000000..55e81b8e1d
--- /dev/null
+++ b/test/export/test_go_test/source_repo/plugins/BUILD_FILE
@@ -0,0 +1,6 @@
+plugin_repo(
+ name = "go",
+ owner = "please-build",
+ plugin = "go-rules",
+ revision = "v1.30.0",
+)
diff --git a/test/export/test_go_test/source_repo/test.go b/test/export/test_go_test/source_repo/test.go
new file mode 100644
index 0000000000..aaf3c0c44b
--- /dev/null
+++ b/test/export/test_go_test/source_repo/test.go
@@ -0,0 +1,11 @@
+package test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSomething(t *testing.T) {
+ assert.NotEqual(t, 0, 1)
+}
diff --git a/test/export/test_go_test/source_repo/third_party/go/BUILD_FILE b/test/export/test_go_test/source_repo/third_party/go/BUILD_FILE
new file mode 100644
index 0000000000..88c12fe2a0
--- /dev/null
+++ b/test/export/test_go_test/source_repo/third_party/go/BUILD_FILE
@@ -0,0 +1,36 @@
+go_toolchain(
+ name = "toolchain",
+ version = "1.26.2",
+)
+
+go_stdlib(name = "std")
+
+go_repo(
+ licences = ["BSD-3-Clause"],
+ module = "github.com/google/go-cmp",
+ version = "v0.6.0",
+)
+
+go_repo(
+ licences = ["MIT"],
+ module = "github.com/stretchr/testify",
+ version = "v1.9.0",
+)
+
+go_repo(
+ licences = ["ISC"],
+ module = "github.com/davecgh/go-spew",
+ version = "v1.1.1",
+)
+
+go_repo(
+ licences = ["BSD-3-Clause"],
+ module = "github.com/pmezard/go-difflib",
+ version = "v1.0.0",
+)
+
+go_repo(
+ licences = ["MIT"],
+ module = "gopkg.in/yaml.v3",
+ version = "v3.0.1",
+)
diff --git a/test/export/test_if/BUILD b/test/export/test_if/BUILD
new file mode 100644
index 0000000000..4af4d89e89
--- /dev/null
+++ b/test/export/test_if/BUILD
@@ -0,0 +1,7 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+# Export a target inside an if block and validate that the else block is correctly trimmed (and replaced with pass).
+please_export_e2e_test(
+ name = "export_if",
+ export_targets = ["//:a"],
+)
diff --git a/test/export/test_if/expected_repo/.plzconfig b/test/export/test_if/expected_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_if/expected_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_if/expected_repo/BUILD_FILE b/test/export/test_if/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..c69bf4c1c0
--- /dev/null
+++ b/test/export/test_if/expected_repo/BUILD_FILE
@@ -0,0 +1,8 @@
+if True:
+ genrule(
+ name = "a",
+ outs = ["a.txt"],
+ cmd = "echo a > $OUT",
+ )
+else:
+ pass # Trimmed during export
diff --git a/test/export/test_if/source_repo/.plzconfig b/test/export/test_if/source_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_if/source_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_if/source_repo/BUILD_FILE b/test/export/test_if/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..ef5650c499
--- /dev/null
+++ b/test/export/test_if/source_repo/BUILD_FILE
@@ -0,0 +1,12 @@
+if True:
+ genrule(
+ name = "a",
+ outs = ["a.txt"],
+ cmd = "echo a > $OUT",
+ )
+else:
+ genrule(
+ name = "b",
+ outs = ["b.txt"],
+ cmd = "echo b > $OUT",
+ )
diff --git a/test/export/test_custom_in_file_def/BUILD b/test/export/test_in_file_build_def/BUILD
similarity index 83%
rename from test/export/test_custom_in_file_def/BUILD
rename to test/export/test_in_file_build_def/BUILD
index 5f361a61dd..a9670e332f 100644
--- a/test/export/test_custom_in_file_def/BUILD
+++ b/test/export/test_in_file_build_def/BUILD
@@ -3,6 +3,6 @@ subinclude("//test/export:export_e2e_test_build_def")
# Export a target generated by a custom build def defined in the same
# BUILD file.
please_export_e2e_test(
- name = "export_custom_target_in_file",
+ name = "export_in_file_build_def",
export_targets = ["//:simple_custom_target"],
)
diff --git a/test/export/test_in_file_build_def/expected_repo/.plzconfig b/test/export/test_in_file_build_def/expected_repo/.plzconfig
new file mode 100644
index 0000000000..8e1ae5655a
--- /dev/null
+++ b/test/export/test_in_file_build_def/expected_repo/.plzconfig
@@ -0,0 +1,3 @@
+[Parse]
+
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_custom_in_file_def/expected_repo/BUILD_FILE b/test/export/test_in_file_build_def/expected_repo/BUILD_FILE
similarity index 100%
rename from test/export/test_custom_in_file_def/expected_repo/BUILD_FILE
rename to test/export/test_in_file_build_def/expected_repo/BUILD_FILE
diff --git a/test/export/test_custom_in_file_def/expected_repo/file.txt b/test/export/test_in_file_build_def/expected_repo/file.txt
similarity index 100%
rename from test/export/test_custom_in_file_def/expected_repo/file.txt
rename to test/export/test_in_file_build_def/expected_repo/file.txt
diff --git a/test/export/test_custom_in_file_def/expected_repo/simple.build_defs b/test/export/test_in_file_build_def/expected_repo/simple.build_defs
similarity index 100%
rename from test/export/test_custom_in_file_def/expected_repo/simple.build_defs
rename to test/export/test_in_file_build_def/expected_repo/simple.build_defs
diff --git a/test/export/test_in_file_build_def/source_repo/.plzconfig b/test/export/test_in_file_build_def/source_repo/.plzconfig
new file mode 100644
index 0000000000..8e1ae5655a
--- /dev/null
+++ b/test/export/test_in_file_build_def/source_repo/.plzconfig
@@ -0,0 +1,3 @@
+[Parse]
+
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_custom_in_file_def/source_repo/BUILD_FILE b/test/export/test_in_file_build_def/source_repo/BUILD_FILE
similarity index 80%
rename from test/export/test_custom_in_file_def/source_repo/BUILD_FILE
rename to test/export/test_in_file_build_def/source_repo/BUILD_FILE
index 6e5251d844..1e7b32583d 100644
--- a/test/export/test_custom_in_file_def/source_repo/BUILD_FILE
+++ b/test/export/test_in_file_build_def/source_repo/BUILD_FILE
@@ -13,7 +13,7 @@ simple_custom_target(
)
simple_custom_target(
- name = "dummy",
- srcs = ["dummy.in"],
- outs = ["dummy.out"],
+ name = "unused",
+ srcs = ["unused.in"],
+ outs = ["unused.out"],
)
diff --git a/test/export/test_custom_in_file_def/source_repo/dummy.in b/test/export/test_in_file_build_def/source_repo/dummy.in
similarity index 100%
rename from test/export/test_custom_in_file_def/source_repo/dummy.in
rename to test/export/test_in_file_build_def/source_repo/dummy.in
diff --git a/test/export/test_custom_in_file_def/source_repo/file.txt b/test/export/test_in_file_build_def/source_repo/file.txt
similarity index 100%
rename from test/export/test_custom_in_file_def/source_repo/file.txt
rename to test/export/test_in_file_build_def/source_repo/file.txt
diff --git a/test/export/test_custom_in_file_def/source_repo/simple.build_defs b/test/export/test_in_file_build_def/source_repo/simple.build_defs
similarity index 100%
rename from test/export/test_custom_in_file_def/source_repo/simple.build_defs
rename to test/export/test_in_file_build_def/source_repo/simple.build_defs
diff --git a/test/export/test_in_file_func_def/BUILD b/test/export/test_in_file_func_def/BUILD
new file mode 100644
index 0000000000..cfb1eabd83
--- /dev/null
+++ b/test/export/test_in_file_func_def/BUILD
@@ -0,0 +1,8 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+# Export a target generated by a custom function def defined in the same
+# BUILD file.
+please_export_e2e_test(
+ name = "export_in_file_function",
+ export_targets = ["//:custom_target"],
+)
diff --git a/test/export/test_in_file_func_def/expected_repo/.plzconfig b/test/export/test_in_file_func_def/expected_repo/.plzconfig
new file mode 100644
index 0000000000..8e1ae5655a
--- /dev/null
+++ b/test/export/test_in_file_func_def/expected_repo/.plzconfig
@@ -0,0 +1,3 @@
+[Parse]
+
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_in_file_func_def/expected_repo/BUILD_FILE b/test/export/test_in_file_func_def/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..4638bb33ab
--- /dev/null
+++ b/test/export/test_in_file_func_def/expected_repo/BUILD_FILE
@@ -0,0 +1,16 @@
+def custom(
+ name:str,
+ srcs:list=[],
+ outs:list=[]):
+ return genrule(
+ name = name,
+ srcs = srcs,
+ outs = outs,
+ cmd = "cat $SRCS > $OUT",
+ )
+
+custom(
+ name = "custom_target",
+ srcs = ["file.txt"],
+ outs = ["file_simple.out"],
+)
diff --git a/test/export/test_in_file_func_def/expected_repo/file.txt b/test/export/test_in_file_func_def/expected_repo/file.txt
new file mode 100644
index 0000000000..9768ee14c2
--- /dev/null
+++ b/test/export/test_in_file_func_def/expected_repo/file.txt
@@ -0,0 +1 @@
+Test source file
diff --git a/test/export/test_in_file_func_def/source_repo/.plzconfig b/test/export/test_in_file_func_def/source_repo/.plzconfig
new file mode 100644
index 0000000000..8e1ae5655a
--- /dev/null
+++ b/test/export/test_in_file_func_def/source_repo/.plzconfig
@@ -0,0 +1,3 @@
+[Parse]
+
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_in_file_func_def/source_repo/BUILD_FILE b/test/export/test_in_file_func_def/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..2f589a44e1
--- /dev/null
+++ b/test/export/test_in_file_func_def/source_repo/BUILD_FILE
@@ -0,0 +1,22 @@
+def custom(
+ name:str,
+ srcs:list=[],
+ outs:list=[]):
+ return genrule(
+ name = name,
+ srcs = srcs,
+ outs = outs,
+ cmd = "cat $SRCS > $OUT",
+ )
+
+custom(
+ name = "custom_target",
+ srcs = ["file.txt"],
+ outs = ["file_simple.out"],
+)
+
+custom(
+ name = "unused",
+ srcs = ["unused.in"],
+ outs = ["unused.out"],
+)
diff --git a/test/export/test_in_file_func_def/source_repo/dummy.in b/test/export/test_in_file_func_def/source_repo/dummy.in
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/export/test_in_file_func_def/source_repo/file.txt b/test/export/test_in_file_func_def/source_repo/file.txt
new file mode 100644
index 0000000000..9768ee14c2
--- /dev/null
+++ b/test/export/test_in_file_func_def/source_repo/file.txt
@@ -0,0 +1 @@
+Test source file
diff --git a/test/export/test_in_file_func_def/source_repo/unused.in b/test/export/test_in_file_func_def/source_repo/unused.in
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/export/test_multiple_targets/source_repo/BUILD_FILE b/test/export/test_multiple_targets/source_repo/BUILD_FILE
index 3dc36004b0..f0ad869d8c 100644
--- a/test/export/test_multiple_targets/source_repo/BUILD_FILE
+++ b/test/export/test_multiple_targets/source_repo/BUILD_FILE
@@ -13,7 +13,7 @@ genrule(
)
genrule(
- name = "dummy",
- outs = ["dummy.out"],
- cmd = "echo dummy > $OUT",
+ name = "unused",
+ outs = ["unused.out"],
+ cmd = "echo unused > $OUT",
)
diff --git a/test/export/test_native_target_with_go_dep/source_repo/third_party/common/BUILD_FILE b/test/export/test_native_target_with_go_dep/source_repo/third_party/common/BUILD_FILE
new file mode 100644
index 0000000000..7237c61982
--- /dev/null
+++ b/test/export/test_native_target_with_go_dep/source_repo/third_party/common/BUILD_FILE
@@ -0,0 +1,7 @@
+export_file(
+ name = "version",
+ src = "version.build_defs",
+ visibility = [
+ "//third_party/...",
+ ],
+)
diff --git a/test/export/test_native_target_with_go_dep/source_repo/third_party/common/version.build_defs b/test/export/test_native_target_with_go_dep/source_repo/third_party/common/version.build_defs
new file mode 100644
index 0000000000..e6a8a7bbd5
--- /dev/null
+++ b/test/export/test_native_target_with_go_dep/source_repo/third_party/common/version.build_defs
@@ -0,0 +1 @@
+LIB_VERSION = "v1.6.0"
diff --git a/test/export/test_native_target_with_go_dep/source_repo/third_party/go/BUILD_FILE b/test/export/test_native_target_with_go_dep/source_repo/third_party/go/BUILD_FILE
index 652eab6b9b..aba53b3922 100644
--- a/test/export/test_native_target_with_go_dep/source_repo/third_party/go/BUILD_FILE
+++ b/test/export/test_native_target_with_go_dep/source_repo/third_party/go/BUILD_FILE
@@ -1,4 +1,7 @@
-subinclude("///go//build_defs:go")
+subinclude(
+ "///go//build_defs:go",
+ "//third_party/common:version",
+)
package(default_visibility = ["PUBLIC"])
@@ -12,6 +15,13 @@ go_stdlib(
name = "std",
)
+go_repo(
+ # unused, unused target
+ name = "uuid",
+ module = "github.com/google/uuid",
+ version = f"{LIB_VERSION}",
+)
+
go_repo(
module = "github.com/google/go-cmp",
version = "v0.5.6",
diff --git a/test/export/test_nested_subinclude/BUILD b/test/export/test_nested_subinclude/BUILD
new file mode 100644
index 0000000000..acce896754
--- /dev/null
+++ b/test/export/test_nested_subinclude/BUILD
@@ -0,0 +1,6 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+please_export_e2e_test(
+ name = "nested_subinclude",
+ export_targets = ["//:target"],
+)
diff --git a/test/export/test_nested_subinclude/expected_repo/.plzconfig b/test/export/test_nested_subinclude/expected_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_nested_subinclude/expected_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_nested_subinclude/expected_repo/BUILD_FILE b/test/export/test_nested_subinclude/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..0ef1d0178a
--- /dev/null
+++ b/test/export/test_nested_subinclude/expected_repo/BUILD_FILE
@@ -0,0 +1,5 @@
+subinclude("//build_defs:primary_build_def")
+
+helper_rule_primary(
+ name = "target",
+)
diff --git a/test/export/test_nested_subinclude/expected_repo/build_defs/BUILD_FILE b/test/export/test_nested_subinclude/expected_repo/build_defs/BUILD_FILE
new file mode 100644
index 0000000000..7e567b845a
--- /dev/null
+++ b/test/export/test_nested_subinclude/expected_repo/build_defs/BUILD_FILE
@@ -0,0 +1,11 @@
+filegroup(
+ name = "primary_build_def",
+ srcs = ["primary.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "secondary_build_def",
+ srcs = ["secondary.build_defs"],
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_nested_subinclude/expected_repo/build_defs/primary.build_defs b/test/export/test_nested_subinclude/expected_repo/build_defs/primary.build_defs
new file mode 100644
index 0000000000..18e484e256
--- /dev/null
+++ b/test/export/test_nested_subinclude/expected_repo/build_defs/primary.build_defs
@@ -0,0 +1,11 @@
+subinclude("//build_defs:secondary_build_def")
+
+def helper_rule_primary(name:str):
+ # Generates the requested target (which has no unparsed dependencies)
+ filegroup(
+ name = name,
+ srcs = [],
+ )
+
+ # Generates an adjacent target (which calls helper_rule and has an unparsed dependency)
+ helper_rule(name + "_adjacent")
diff --git a/test/export/test_nested_subinclude/expected_repo/build_defs/secondary.build_defs b/test/export/test_nested_subinclude/expected_repo/build_defs/secondary.build_defs
new file mode 100644
index 0000000000..8f7794b0d6
--- /dev/null
+++ b/test/export/test_nested_subinclude/expected_repo/build_defs/secondary.build_defs
@@ -0,0 +1,7 @@
+def helper_rule(name:str):
+ build_rule(
+ name = name,
+ outs = [name + ".out"],
+ cmd = "sh $TOOL > $OUT",
+ tools = ["//tools"],
+ )
diff --git a/test/export/test_nested_subinclude/expected_repo/tools/BUILD_FILE b/test/export/test_nested_subinclude/expected_repo/tools/BUILD_FILE
new file mode 100644
index 0000000000..215281ac57
--- /dev/null
+++ b/test/export/test_nested_subinclude/expected_repo/tools/BUILD_FILE
@@ -0,0 +1,5 @@
+filegroup(
+ name = "tools",
+ srcs = ["tools.sh"],
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_nested_subinclude/expected_repo/tools/tools.sh b/test/export/test_nested_subinclude/expected_repo/tools/tools.sh
new file mode 100644
index 0000000000..b77678ef4f
--- /dev/null
+++ b/test/export/test_nested_subinclude/expected_repo/tools/tools.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+echo "nested dependency success"
diff --git a/test/export/test_nested_subinclude/source_repo/.plzconfig b/test/export/test_nested_subinclude/source_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_nested_subinclude/source_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_nested_subinclude/source_repo/BUILD_FILE b/test/export/test_nested_subinclude/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..108fcfb57c
--- /dev/null
+++ b/test/export/test_nested_subinclude/source_repo/BUILD_FILE
@@ -0,0 +1,13 @@
+subinclude(
+ "//build_defs:primary_build_def",
+ "//build_defs:unused_build_def",
+)
+
+helper_rule_primary(
+ name = "target",
+)
+
+filegroup(
+ name = "unused",
+ srcs = ["file.txt"],
+)
diff --git a/test/export/test_nested_subinclude/source_repo/build_defs/BUILD_FILE b/test/export/test_nested_subinclude/source_repo/build_defs/BUILD_FILE
new file mode 100644
index 0000000000..b65694fcb9
--- /dev/null
+++ b/test/export/test_nested_subinclude/source_repo/build_defs/BUILD_FILE
@@ -0,0 +1,17 @@
+filegroup(
+ name = "primary_build_def",
+ srcs = ["primary.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "secondary_build_def",
+ srcs = ["secondary.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "unused_build_def",
+ srcs = ["unused.build_defs"],
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_nested_subinclude/source_repo/build_defs/primary.build_defs b/test/export/test_nested_subinclude/source_repo/build_defs/primary.build_defs
new file mode 100644
index 0000000000..18e484e256
--- /dev/null
+++ b/test/export/test_nested_subinclude/source_repo/build_defs/primary.build_defs
@@ -0,0 +1,11 @@
+subinclude("//build_defs:secondary_build_def")
+
+def helper_rule_primary(name:str):
+ # Generates the requested target (which has no unparsed dependencies)
+ filegroup(
+ name = name,
+ srcs = [],
+ )
+
+ # Generates an adjacent target (which calls helper_rule and has an unparsed dependency)
+ helper_rule(name + "_adjacent")
diff --git a/test/export/test_nested_subinclude/source_repo/build_defs/secondary.build_defs b/test/export/test_nested_subinclude/source_repo/build_defs/secondary.build_defs
new file mode 100644
index 0000000000..8f7794b0d6
--- /dev/null
+++ b/test/export/test_nested_subinclude/source_repo/build_defs/secondary.build_defs
@@ -0,0 +1,7 @@
+def helper_rule(name:str):
+ build_rule(
+ name = name,
+ outs = [name + ".out"],
+ cmd = "sh $TOOL > $OUT",
+ tools = ["//tools"],
+ )
diff --git a/test/export/test_nested_subinclude/source_repo/build_defs/unused.build_defs b/test/export/test_nested_subinclude/source_repo/build_defs/unused.build_defs
new file mode 100644
index 0000000000..072217364b
--- /dev/null
+++ b/test/export/test_nested_subinclude/source_repo/build_defs/unused.build_defs
@@ -0,0 +1,2 @@
+def unused_func():
+ pass
diff --git a/test/export/test_nested_subinclude/source_repo/file.txt b/test/export/test_nested_subinclude/source_repo/file.txt
new file mode 100644
index 0000000000..d95f3ad14d
--- /dev/null
+++ b/test/export/test_nested_subinclude/source_repo/file.txt
@@ -0,0 +1 @@
+content
diff --git a/test/export/test_nested_subinclude/source_repo/tools/BUILD_FILE b/test/export/test_nested_subinclude/source_repo/tools/BUILD_FILE
new file mode 100644
index 0000000000..215281ac57
--- /dev/null
+++ b/test/export/test_nested_subinclude/source_repo/tools/BUILD_FILE
@@ -0,0 +1,5 @@
+filegroup(
+ name = "tools",
+ srcs = ["tools.sh"],
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_nested_subinclude/source_repo/tools/tools.sh b/test/export/test_nested_subinclude/source_repo/tools/tools.sh
new file mode 100644
index 0000000000..b77678ef4f
--- /dev/null
+++ b/test/export/test_nested_subinclude/source_repo/tools/tools.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+echo "nested dependency success"
diff --git a/test/export/test_subinclude_preloaded/BUILD b/test/export/test_subinclude_preloaded/BUILD
new file mode 100644
index 0000000000..4b1f6971d0
--- /dev/null
+++ b/test/export/test_subinclude_preloaded/BUILD
@@ -0,0 +1,8 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+# Verifies that `plz export` correctly processes a subincluded build definition target
+# that in turn subincludes a preloaded target.
+please_export_e2e_test(
+ name = "subinclude_preloaded_export_test",
+ export_targets = ["//:file"],
+)
diff --git a/test/export/test_subinclude_preloaded/expected_repo/.plzconfig b/test/export/test_subinclude_preloaded/expected_repo/.plzconfig
new file mode 100644
index 0000000000..0b1b781c2d
--- /dev/null
+++ b/test/export/test_subinclude_preloaded/expected_repo/.plzconfig
@@ -0,0 +1,3 @@
+[parse]
+BuildFileName = BUILD_FILE
+preloadsubincludes = //build_defs:preloaded_build_def
diff --git a/test/export/test_subinclude_preloaded/expected_repo/BUILD_FILE b/test/export/test_subinclude_preloaded/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..0b66bcffbc
--- /dev/null
+++ b/test/export/test_subinclude_preloaded/expected_repo/BUILD_FILE
@@ -0,0 +1,6 @@
+subinclude("//build_defs:subincluded_build_def")
+
+custom_file(
+ name = "file",
+ src = "file.txt",
+)
diff --git a/test/export/test_subinclude_preloaded/expected_repo/build_defs/BUILD_FILE b/test/export/test_subinclude_preloaded/expected_repo/build_defs/BUILD_FILE
new file mode 100644
index 0000000000..847d65de60
--- /dev/null
+++ b/test/export/test_subinclude_preloaded/expected_repo/build_defs/BUILD_FILE
@@ -0,0 +1,11 @@
+filegroup(
+ name = "preloaded_build_def",
+ srcs = ["preloaded.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "subincluded_build_def",
+ srcs = ["subincluded.build_defs"],
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_subinclude_preloaded/expected_repo/build_defs/preloaded.build_defs b/test/export/test_subinclude_preloaded/expected_repo/build_defs/preloaded.build_defs
new file mode 100644
index 0000000000..b1f090cec8
--- /dev/null
+++ b/test/export/test_subinclude_preloaded/expected_repo/build_defs/preloaded.build_defs
@@ -0,0 +1 @@
+# preloaded build def
diff --git a/test/export/test_subinclude_preloaded/expected_repo/build_defs/subincluded.build_defs b/test/export/test_subinclude_preloaded/expected_repo/build_defs/subincluded.build_defs
new file mode 100644
index 0000000000..7cef9cdf29
--- /dev/null
+++ b/test/export/test_subinclude_preloaded/expected_repo/build_defs/subincluded.build_defs
@@ -0,0 +1,7 @@
+subinclude("//build_defs:preloaded_build_def")
+
+def custom_file(name, src):
+ filegroup(
+ name = name,
+ srcs = [src],
+ )
diff --git a/test/export/test_subinclude_preloaded/expected_repo/file.txt b/test/export/test_subinclude_preloaded/expected_repo/file.txt
new file mode 100644
index 0000000000..f73f3093ff
--- /dev/null
+++ b/test/export/test_subinclude_preloaded/expected_repo/file.txt
@@ -0,0 +1 @@
+file
diff --git a/test/export/test_subinclude_preloaded/source_repo/.plzconfig b/test/export/test_subinclude_preloaded/source_repo/.plzconfig
new file mode 100644
index 0000000000..0b1b781c2d
--- /dev/null
+++ b/test/export/test_subinclude_preloaded/source_repo/.plzconfig
@@ -0,0 +1,3 @@
+[parse]
+BuildFileName = BUILD_FILE
+preloadsubincludes = //build_defs:preloaded_build_def
diff --git a/test/export/test_subinclude_preloaded/source_repo/BUILD_FILE b/test/export/test_subinclude_preloaded/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..ef483dbc86
--- /dev/null
+++ b/test/export/test_subinclude_preloaded/source_repo/BUILD_FILE
@@ -0,0 +1,11 @@
+subinclude("//build_defs:subincluded_build_def")
+
+custom_file(
+ name = "file",
+ src = "file.txt",
+)
+
+custom_file(
+ name = "unused",
+ src = "unused.txt",
+)
diff --git a/test/export/test_subinclude_preloaded/source_repo/build_defs/BUILD_FILE b/test/export/test_subinclude_preloaded/source_repo/build_defs/BUILD_FILE
new file mode 100644
index 0000000000..847d65de60
--- /dev/null
+++ b/test/export/test_subinclude_preloaded/source_repo/build_defs/BUILD_FILE
@@ -0,0 +1,11 @@
+filegroup(
+ name = "preloaded_build_def",
+ srcs = ["preloaded.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "subincluded_build_def",
+ srcs = ["subincluded.build_defs"],
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_subinclude_preloaded/source_repo/build_defs/preloaded.build_defs b/test/export/test_subinclude_preloaded/source_repo/build_defs/preloaded.build_defs
new file mode 100644
index 0000000000..b1f090cec8
--- /dev/null
+++ b/test/export/test_subinclude_preloaded/source_repo/build_defs/preloaded.build_defs
@@ -0,0 +1 @@
+# preloaded build def
diff --git a/test/export/test_subinclude_preloaded/source_repo/build_defs/subincluded.build_defs b/test/export/test_subinclude_preloaded/source_repo/build_defs/subincluded.build_defs
new file mode 100644
index 0000000000..7cef9cdf29
--- /dev/null
+++ b/test/export/test_subinclude_preloaded/source_repo/build_defs/subincluded.build_defs
@@ -0,0 +1,7 @@
+subinclude("//build_defs:preloaded_build_def")
+
+def custom_file(name, src):
+ filegroup(
+ name = name,
+ srcs = [src],
+ )
diff --git a/test/export/test_subinclude_preloaded/source_repo/file.txt b/test/export/test_subinclude_preloaded/source_repo/file.txt
new file mode 100644
index 0000000000..f73f3093ff
--- /dev/null
+++ b/test/export/test_subinclude_preloaded/source_repo/file.txt
@@ -0,0 +1 @@
+file
diff --git a/test/export/test_subinclude_preloaded/source_repo/unused.txt b/test/export/test_subinclude_preloaded/source_repo/unused.txt
new file mode 100644
index 0000000000..9b9778a8c9
--- /dev/null
+++ b/test/export/test_subinclude_preloaded/source_repo/unused.txt
@@ -0,0 +1 @@
+this is unused
diff --git a/test/export/test_subinclude_trimming/BUILD b/test/export/test_subinclude_trimming/BUILD
new file mode 100644
index 0000000000..ae31a718eb
--- /dev/null
+++ b/test/export/test_subinclude_trimming/BUILD
@@ -0,0 +1,10 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+# Export a target generated by a custom build def and validate that any unused subincludes are trimmed.
+please_export_e2e_test(
+ name = "export_subinclude_trimming",
+ export_targets = [
+ "//:simple_custom_target",
+ "//:variables_target",
+ ],
+)
diff --git a/test/export/test_subinclude_trimming/expected_repo/.plzconfig b/test/export/test_subinclude_trimming/expected_repo/.plzconfig
new file mode 100644
index 0000000000..8e1ae5655a
--- /dev/null
+++ b/test/export/test_subinclude_trimming/expected_repo/.plzconfig
@@ -0,0 +1,3 @@
+[Parse]
+
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_subinclude_trimming/expected_repo/BUILD_FILE b/test/export/test_subinclude_trimming/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..08de91e888
--- /dev/null
+++ b/test/export/test_subinclude_trimming/expected_repo/BUILD_FILE
@@ -0,0 +1,35 @@
+subinclude("//build_defs:simple_build_def")
+
+simple_custom_target(
+ name = "simple_custom_target",
+ srcs = ["file.txt"],
+ outs = ["file_simple.out"],
+)
+
+subinclude(
+ "//build_defs:var1_build_def",
+ "//build_defs:var2_build_def",
+)
+
+comma_separated_vars = ",".join([
+ var1,
+ var2,
+])
+
+genrule(
+ name = "variables_target",
+ outs = ["variable.out"],
+ cmd = f'echo "{comma_separated_vars}" > $OUT',
+)
+
+subinclude("//build_defs:var3_build_def")
+
+message = "Testing {service} on {env}".format(
+ env = "production",
+ service = var3,
+)
+
+subinclude("//build_defs:versions_build_def")
+
+for version, name in VERSIONS.items():
+ pass # Trimmed during export
diff --git a/test/export/test_subinclude_trimming/expected_repo/build_defs/BUILD_FILE b/test/export/test_subinclude_trimming/expected_repo/build_defs/BUILD_FILE
new file mode 100644
index 0000000000..55ab29141b
--- /dev/null
+++ b/test/export/test_subinclude_trimming/expected_repo/build_defs/BUILD_FILE
@@ -0,0 +1,29 @@
+filegroup(
+ name = "simple_build_def",
+ srcs = ["simple.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "var1_build_def",
+ srcs = ["var1.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "var2_build_def",
+ srcs = ["var2.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "var3_build_def",
+ srcs = ["var3.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "versions_build_def",
+ srcs = ["versions.build_defs"],
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_subinclude_trimming/expected_repo/build_defs/simple.build_defs b/test/export/test_subinclude_trimming/expected_repo/build_defs/simple.build_defs
new file mode 100644
index 0000000000..8fe3a6021b
--- /dev/null
+++ b/test/export/test_subinclude_trimming/expected_repo/build_defs/simple.build_defs
@@ -0,0 +1,10 @@
+def simple_custom_target(
+ name:str,
+ srcs:list=[],
+ outs:list=[]):
+ return genrule(
+ name = name,
+ srcs = srcs,
+ outs = outs,
+ cmd = "cat $SRCS > $OUT",
+ )
diff --git a/test/export/test_subinclude_trimming/expected_repo/build_defs/var1.build_defs b/test/export/test_subinclude_trimming/expected_repo/build_defs/var1.build_defs
new file mode 100644
index 0000000000..07814f1e90
--- /dev/null
+++ b/test/export/test_subinclude_trimming/expected_repo/build_defs/var1.build_defs
@@ -0,0 +1 @@
+var1 = "Variable 1"
diff --git a/test/export/test_subinclude_trimming/expected_repo/build_defs/var2.build_defs b/test/export/test_subinclude_trimming/expected_repo/build_defs/var2.build_defs
new file mode 100644
index 0000000000..b17ada62b4
--- /dev/null
+++ b/test/export/test_subinclude_trimming/expected_repo/build_defs/var2.build_defs
@@ -0,0 +1 @@
+var2 = "Variable 2"
diff --git a/test/export/test_subinclude_trimming/expected_repo/build_defs/var3.build_defs b/test/export/test_subinclude_trimming/expected_repo/build_defs/var3.build_defs
new file mode 100644
index 0000000000..c5cfb6d90c
--- /dev/null
+++ b/test/export/test_subinclude_trimming/expected_repo/build_defs/var3.build_defs
@@ -0,0 +1 @@
+var3 = "Variable 3"
diff --git a/test/export/test_subinclude_trimming/expected_repo/build_defs/versions.build_defs b/test/export/test_subinclude_trimming/expected_repo/build_defs/versions.build_defs
new file mode 100644
index 0000000000..e565a23f8f
--- /dev/null
+++ b/test/export/test_subinclude_trimming/expected_repo/build_defs/versions.build_defs
@@ -0,0 +1,4 @@
+VERSIONS = {
+ "1.0": "v1",
+ "1.1": "v1.1",
+}
diff --git a/test/export/test_subinclude_trimming/expected_repo/file.txt b/test/export/test_subinclude_trimming/expected_repo/file.txt
new file mode 100644
index 0000000000..9768ee14c2
--- /dev/null
+++ b/test/export/test_subinclude_trimming/expected_repo/file.txt
@@ -0,0 +1 @@
+Test source file
diff --git a/test/export/test_subinclude_trimming/source_repo/.plzconfig b/test/export/test_subinclude_trimming/source_repo/.plzconfig
new file mode 100644
index 0000000000..8e1ae5655a
--- /dev/null
+++ b/test/export/test_subinclude_trimming/source_repo/.plzconfig
@@ -0,0 +1,3 @@
+[Parse]
+
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_subinclude_trimming/source_repo/BUILD_FILE b/test/export/test_subinclude_trimming/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..b11e6c2b9a
--- /dev/null
+++ b/test/export/test_subinclude_trimming/source_repo/BUILD_FILE
@@ -0,0 +1,59 @@
+subinclude(
+ "//build_defs:simple_build_def",
+ "//build_defs:unused_build_def",
+)
+
+simple_custom_target(
+ name = "simple_custom_target",
+ srcs = ["file.txt"],
+ outs = ["file_simple.out"],
+)
+
+unused_target(
+ name = "unused",
+ outs = ["unused.out"],
+)
+
+subinclude(
+ "//build_defs:var1_build_def",
+ "//build_defs:var2_build_def",
+)
+
+comma_separated_vars = ",".join([
+ var1,
+ var2,
+])
+
+genrule(
+ name = "variables_target",
+ outs = ["variable.out"],
+ cmd = f'echo "{comma_separated_vars}" > $OUT',
+)
+
+genrule(
+ name = "unused_variables_target",
+ outs = ["unused_variable.out"],
+ cmd = f'echo "{unused_var}" > $OUT',
+)
+
+subinclude("//build_defs:var3_build_def")
+
+message = "Testing {service} on {env}".format(
+ env = "production",
+ service = var3,
+)
+
+genrule(
+ name = "format_target",
+ outs = ["format.out"],
+ cmd = f'echo "{message}" > $OUT',
+)
+
+subinclude("//build_defs:versions_build_def")
+
+for version, name in VERSIONS.items():
+ genrule(
+ name = f"version-{version}",
+ outs = [f"version-{version}.out"],
+ cmd = f'echo "{name}" > $OUT',
+ )
diff --git a/test/export/test_subinclude_trimming/source_repo/build_defs/BUILD_FILE b/test/export/test_subinclude_trimming/source_repo/build_defs/BUILD_FILE
new file mode 100644
index 0000000000..b1fdef3b01
--- /dev/null
+++ b/test/export/test_subinclude_trimming/source_repo/build_defs/BUILD_FILE
@@ -0,0 +1,35 @@
+filegroup(
+ name = "simple_build_def",
+ srcs = ["simple.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "var1_build_def",
+ srcs = ["var1.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "var2_build_def",
+ srcs = ["var2.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "var3_build_def",
+ srcs = ["var3.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "unused_build_def",
+ srcs = ["unused.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "versions_build_def",
+ srcs = ["versions.build_defs"],
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_subinclude_trimming/source_repo/build_defs/simple.build_defs b/test/export/test_subinclude_trimming/source_repo/build_defs/simple.build_defs
new file mode 100644
index 0000000000..8fe3a6021b
--- /dev/null
+++ b/test/export/test_subinclude_trimming/source_repo/build_defs/simple.build_defs
@@ -0,0 +1,10 @@
+def simple_custom_target(
+ name:str,
+ srcs:list=[],
+ outs:list=[]):
+ return genrule(
+ name = name,
+ srcs = srcs,
+ outs = outs,
+ cmd = "cat $SRCS > $OUT",
+ )
diff --git a/test/export/test_subinclude_trimming/source_repo/build_defs/unused.build_defs b/test/export/test_subinclude_trimming/source_repo/build_defs/unused.build_defs
new file mode 100644
index 0000000000..6c1a024f34
--- /dev/null
+++ b/test/export/test_subinclude_trimming/source_repo/build_defs/unused.build_defs
@@ -0,0 +1,10 @@
+def unused_target(
+ name:str,
+ outs:list=[]):
+ return genrule(
+ name = name,
+ outs = outs,
+ cmd = "echo unused > $OUT",
+ )
+
+unused_var = "Unused Variable"
diff --git a/test/export/test_subinclude_trimming/source_repo/build_defs/var1.build_defs b/test/export/test_subinclude_trimming/source_repo/build_defs/var1.build_defs
new file mode 100644
index 0000000000..07814f1e90
--- /dev/null
+++ b/test/export/test_subinclude_trimming/source_repo/build_defs/var1.build_defs
@@ -0,0 +1 @@
+var1 = "Variable 1"
diff --git a/test/export/test_subinclude_trimming/source_repo/build_defs/var2.build_defs b/test/export/test_subinclude_trimming/source_repo/build_defs/var2.build_defs
new file mode 100644
index 0000000000..b17ada62b4
--- /dev/null
+++ b/test/export/test_subinclude_trimming/source_repo/build_defs/var2.build_defs
@@ -0,0 +1 @@
+var2 = "Variable 2"
diff --git a/test/export/test_subinclude_trimming/source_repo/build_defs/var3.build_defs b/test/export/test_subinclude_trimming/source_repo/build_defs/var3.build_defs
new file mode 100644
index 0000000000..c5cfb6d90c
--- /dev/null
+++ b/test/export/test_subinclude_trimming/source_repo/build_defs/var3.build_defs
@@ -0,0 +1 @@
+var3 = "Variable 3"
diff --git a/test/export/test_subinclude_trimming/source_repo/build_defs/versions.build_defs b/test/export/test_subinclude_trimming/source_repo/build_defs/versions.build_defs
new file mode 100644
index 0000000000..e565a23f8f
--- /dev/null
+++ b/test/export/test_subinclude_trimming/source_repo/build_defs/versions.build_defs
@@ -0,0 +1,4 @@
+VERSIONS = {
+ "1.0": "v1",
+ "1.1": "v1.1",
+}
diff --git a/test/export/test_subinclude_trimming/source_repo/file.txt b/test/export/test_subinclude_trimming/source_repo/file.txt
new file mode 100644
index 0000000000..9768ee14c2
--- /dev/null
+++ b/test/export/test_subinclude_trimming/source_repo/file.txt
@@ -0,0 +1 @@
+Test source file
diff --git a/test/export/test_subinclude_unused/BUILD b/test/export/test_subinclude_unused/BUILD
new file mode 100644
index 0000000000..b796cd33ab
--- /dev/null
+++ b/test/export/test_subinclude_unused/BUILD
@@ -0,0 +1,9 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+# Verifies that when a build definition statically subincludes another target but does not
+# actively invoke its methods, the subincluded target is still fully exported to ensure
+# static subinclude parsing succeeds in the exported repository.
+please_export_e2e_test(
+ name = "subinclude_unused_export_test",
+ export_targets = ["//:file"],
+)
diff --git a/test/export/test_subinclude_unused/expected_repo/.plzconfig b/test/export/test_subinclude_unused/expected_repo/.plzconfig
new file mode 100644
index 0000000000..ea85e4f734
--- /dev/null
+++ b/test/export/test_subinclude_unused/expected_repo/.plzconfig
@@ -0,0 +1,2 @@
+[parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_subinclude_unused/expected_repo/BUILD_FILE b/test/export/test_subinclude_unused/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..0b66bcffbc
--- /dev/null
+++ b/test/export/test_subinclude_unused/expected_repo/BUILD_FILE
@@ -0,0 +1,6 @@
+subinclude("//build_defs:subincluded_build_def")
+
+custom_file(
+ name = "file",
+ src = "file.txt",
+)
diff --git a/test/export/test_subinclude_unused/expected_repo/build_defs/BUILD_FILE b/test/export/test_subinclude_unused/expected_repo/build_defs/BUILD_FILE
new file mode 100644
index 0000000000..86e5b46e15
--- /dev/null
+++ b/test/export/test_subinclude_unused/expected_repo/build_defs/BUILD_FILE
@@ -0,0 +1,11 @@
+filegroup(
+ name = "subincluded_build_def",
+ srcs = ["subincluded.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "unused_build_def",
+ srcs = ["unused.build_defs"],
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_subinclude_unused/expected_repo/build_defs/subincluded.build_defs b/test/export/test_subinclude_unused/expected_repo/build_defs/subincluded.build_defs
new file mode 100644
index 0000000000..a07d13ee84
--- /dev/null
+++ b/test/export/test_subinclude_unused/expected_repo/build_defs/subincluded.build_defs
@@ -0,0 +1,7 @@
+subinclude("//build_defs:unused_build_def")
+
+def custom_file(name, src):
+ filegroup(
+ name = name,
+ srcs = [src],
+ )
diff --git a/test/export/test_subinclude_unused/expected_repo/build_defs/unused.build_defs b/test/export/test_subinclude_unused/expected_repo/build_defs/unused.build_defs
new file mode 100644
index 0000000000..b8a3a700f9
--- /dev/null
+++ b/test/export/test_subinclude_unused/expected_repo/build_defs/unused.build_defs
@@ -0,0 +1,6 @@
+# unused build def content
+
+def unused_helper_method(name):
+ return filegroup(
+ name = name,
+ )
diff --git a/test/export/test_subinclude_unused/expected_repo/file.txt b/test/export/test_subinclude_unused/expected_repo/file.txt
new file mode 100644
index 0000000000..f73f3093ff
--- /dev/null
+++ b/test/export/test_subinclude_unused/expected_repo/file.txt
@@ -0,0 +1 @@
+file
diff --git a/test/export/test_subinclude_unused/source_repo/.plzconfig b/test/export/test_subinclude_unused/source_repo/.plzconfig
new file mode 100644
index 0000000000..ea85e4f734
--- /dev/null
+++ b/test/export/test_subinclude_unused/source_repo/.plzconfig
@@ -0,0 +1,2 @@
+[parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_subinclude_unused/source_repo/BUILD_FILE b/test/export/test_subinclude_unused/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..ef483dbc86
--- /dev/null
+++ b/test/export/test_subinclude_unused/source_repo/BUILD_FILE
@@ -0,0 +1,11 @@
+subinclude("//build_defs:subincluded_build_def")
+
+custom_file(
+ name = "file",
+ src = "file.txt",
+)
+
+custom_file(
+ name = "unused",
+ src = "unused.txt",
+)
diff --git a/test/export/test_subinclude_unused/source_repo/build_defs/BUILD_FILE b/test/export/test_subinclude_unused/source_repo/build_defs/BUILD_FILE
new file mode 100644
index 0000000000..86e5b46e15
--- /dev/null
+++ b/test/export/test_subinclude_unused/source_repo/build_defs/BUILD_FILE
@@ -0,0 +1,11 @@
+filegroup(
+ name = "subincluded_build_def",
+ srcs = ["subincluded.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "unused_build_def",
+ srcs = ["unused.build_defs"],
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_subinclude_unused/source_repo/build_defs/subincluded.build_defs b/test/export/test_subinclude_unused/source_repo/build_defs/subincluded.build_defs
new file mode 100644
index 0000000000..a07d13ee84
--- /dev/null
+++ b/test/export/test_subinclude_unused/source_repo/build_defs/subincluded.build_defs
@@ -0,0 +1,7 @@
+subinclude("//build_defs:unused_build_def")
+
+def custom_file(name, src):
+ filegroup(
+ name = name,
+ srcs = [src],
+ )
diff --git a/test/export/test_subinclude_unused/source_repo/build_defs/unused.build_defs b/test/export/test_subinclude_unused/source_repo/build_defs/unused.build_defs
new file mode 100644
index 0000000000..b8a3a700f9
--- /dev/null
+++ b/test/export/test_subinclude_unused/source_repo/build_defs/unused.build_defs
@@ -0,0 +1,6 @@
+# unused build def content
+
+def unused_helper_method(name):
+ return filegroup(
+ name = name,
+ )
diff --git a/test/export/test_subinclude_unused/source_repo/file.txt b/test/export/test_subinclude_unused/source_repo/file.txt
new file mode 100644
index 0000000000..f73f3093ff
--- /dev/null
+++ b/test/export/test_subinclude_unused/source_repo/file.txt
@@ -0,0 +1 @@
+file
diff --git a/test/export/test_subinclude_unused/source_repo/unused.txt b/test/export/test_subinclude_unused/source_repo/unused.txt
new file mode 100644
index 0000000000..b2c475fedd
--- /dev/null
+++ b/test/export/test_subinclude_unused/source_repo/unused.txt
@@ -0,0 +1 @@
+unused
diff --git a/test/export/test_subinclude_variable/BUILD b/test/export/test_subinclude_variable/BUILD
new file mode 100644
index 0000000000..2fcedfa352
--- /dev/null
+++ b/test/export/test_subinclude_variable/BUILD
@@ -0,0 +1,6 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+please_export_e2e_test(
+ name = "subinclude_variable",
+ export_targets = ["//:test"],
+)
diff --git a/test/export/test_subinclude_variable/expected_repo/.plzconfig b/test/export/test_subinclude_variable/expected_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_subinclude_variable/expected_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_subinclude_variable/expected_repo/BUILD_FILE b/test/export/test_subinclude_variable/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..646febc216
--- /dev/null
+++ b/test/export/test_subinclude_variable/expected_repo/BUILD_FILE
@@ -0,0 +1,13 @@
+filegroup(
+ name = "defs",
+ srcs = ["defs.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+subinclude(":defs")
+
+genrule(
+ name = "test",
+ outs = ["test.txt"],
+ cmd = f"echo {VAR} > $OUT",
+)
diff --git a/test/export/test_subinclude_variable/expected_repo/defs.build_defs b/test/export/test_subinclude_variable/expected_repo/defs.build_defs
new file mode 100644
index 0000000000..cdd62f88ae
--- /dev/null
+++ b/test/export/test_subinclude_variable/expected_repo/defs.build_defs
@@ -0,0 +1 @@
+VAR = "hello"
diff --git a/test/export/test_subinclude_variable/source_repo/.plzconfig b/test/export/test_subinclude_variable/source_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_subinclude_variable/source_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_subinclude_variable/source_repo/BUILD_FILE b/test/export/test_subinclude_variable/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..c63a0e733b
--- /dev/null
+++ b/test/export/test_subinclude_variable/source_repo/BUILD_FILE
@@ -0,0 +1,19 @@
+filegroup(
+ name = "defs",
+ srcs = ["defs.build_defs"],
+ visibility = ["PUBLIC"],
+)
+
+subinclude(":defs")
+
+genrule(
+ name = "test",
+ outs = ["test.txt"],
+ cmd = f"echo {VAR} > $OUT",
+)
+
+genrule(
+ name = "unused",
+ outs = ["unused.txt"],
+ cmd = "touch $OUT",
+)
diff --git a/test/export/test_subinclude_variable/source_repo/defs.build_defs b/test/export/test_subinclude_variable/source_repo/defs.build_defs
new file mode 100644
index 0000000000..cdd62f88ae
--- /dev/null
+++ b/test/export/test_subinclude_variable/source_repo/defs.build_defs
@@ -0,0 +1 @@
+VAR = "hello"
diff --git a/test/export/test_subrepo_subtarget/BUILD b/test/export/test_subrepo_subtarget/BUILD
new file mode 100644
index 0000000000..90bbf0c233
--- /dev/null
+++ b/test/export/test_subrepo_subtarget/BUILD
@@ -0,0 +1,8 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+# Verifies that `plz export` correctly performs lookups to resolve internally generated sub-targets
+# belonging to packages defined inside subrepos.
+please_export_e2e_test(
+ name = "subrepo_subtarget_export_test",
+ export_targets = ["//:target"],
+)
diff --git a/test/export/test_subrepo_subtarget/expected_repo/.plzconfig b/test/export/test_subrepo_subtarget/expected_repo/.plzconfig
new file mode 100644
index 0000000000..ea85e4f734
--- /dev/null
+++ b/test/export/test_subrepo_subtarget/expected_repo/.plzconfig
@@ -0,0 +1,2 @@
+[parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_subrepo_subtarget/expected_repo/BUILD_FILE b/test/export/test_subrepo_subtarget/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..fe636b2061
--- /dev/null
+++ b/test/export/test_subrepo_subtarget/expected_repo/BUILD_FILE
@@ -0,0 +1,12 @@
+subrepo(
+ name = "my_subrepo",
+ path = "subrepo",
+)
+
+filegroup(
+ name = "target",
+ srcs = [
+ "file.txt",
+ "///my_subrepo//:_foo#sub",
+ ],
+)
diff --git a/test/export/test_subrepo_subtarget/expected_repo/file.txt b/test/export/test_subrepo_subtarget/expected_repo/file.txt
new file mode 100644
index 0000000000..3b18e512db
--- /dev/null
+++ b/test/export/test_subrepo_subtarget/expected_repo/file.txt
@@ -0,0 +1 @@
+hello world
diff --git a/test/export/test_subrepo_subtarget/expected_repo/subrepo/BUILD_FILE b/test/export/test_subrepo_subtarget/expected_repo/subrepo/BUILD_FILE
new file mode 100644
index 0000000000..7771998a58
--- /dev/null
+++ b/test/export/test_subrepo_subtarget/expected_repo/subrepo/BUILD_FILE
@@ -0,0 +1,5 @@
+filegroup(
+ name = "_foo#sub",
+ srcs = ["BUILD_FILE"],
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_subrepo_subtarget/source_repo/.plzconfig b/test/export/test_subrepo_subtarget/source_repo/.plzconfig
new file mode 100644
index 0000000000..ea85e4f734
--- /dev/null
+++ b/test/export/test_subrepo_subtarget/source_repo/.plzconfig
@@ -0,0 +1,2 @@
+[parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_subrepo_subtarget/source_repo/BUILD_FILE b/test/export/test_subrepo_subtarget/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..4bfd9ef41a
--- /dev/null
+++ b/test/export/test_subrepo_subtarget/source_repo/BUILD_FILE
@@ -0,0 +1,17 @@
+subrepo(
+ name = "my_subrepo",
+ path = "subrepo",
+)
+
+filegroup(
+ name = "target",
+ srcs = [
+ "file.txt",
+ "///my_subrepo//:_foo#sub",
+ ],
+)
+
+filegroup(
+ name = "unused",
+ srcs = ["unused.txt"],
+)
diff --git a/test/export/test_subrepo_subtarget/source_repo/file.txt b/test/export/test_subrepo_subtarget/source_repo/file.txt
new file mode 100644
index 0000000000..3b18e512db
--- /dev/null
+++ b/test/export/test_subrepo_subtarget/source_repo/file.txt
@@ -0,0 +1 @@
+hello world
diff --git a/test/export/test_subrepo_subtarget/source_repo/subrepo/BUILD_FILE b/test/export/test_subrepo_subtarget/source_repo/subrepo/BUILD_FILE
new file mode 100644
index 0000000000..6cc5679bd5
--- /dev/null
+++ b/test/export/test_subrepo_subtarget/source_repo/subrepo/BUILD_FILE
@@ -0,0 +1,11 @@
+filegroup(
+ name = "package",
+ srcs = ["BUILD_FILE"],
+ visibility = ["PUBLIC"],
+)
+
+filegroup(
+ name = "_foo#sub",
+ srcs = ["BUILD_FILE"],
+ visibility = ["PUBLIC"],
+)
diff --git a/test/export/test_subrepo_subtarget/source_repo/unused.txt b/test/export/test_subrepo_subtarget/source_repo/unused.txt
new file mode 100644
index 0000000000..b2c475fedd
--- /dev/null
+++ b/test/export/test_subrepo_subtarget/source_repo/unused.txt
@@ -0,0 +1 @@
+unused
diff --git a/test/export/test_target_variable_trimming/BUILD b/test/export/test_target_variable_trimming/BUILD
new file mode 100644
index 0000000000..2744a85ae6
--- /dev/null
+++ b/test/export/test_target_variable_trimming/BUILD
@@ -0,0 +1,9 @@
+subinclude("//test/export:export_e2e_test_build_def")
+
+# This test validates that if a target is assigned to a variable,
+# the variable assignment statement is correctly trimmed if the target
+# referencing it is unused/not exported, and kept if the target is exported.
+please_export_e2e_test(
+ name = "target_variable_trimming",
+ export_targets = ["//:used"],
+)
diff --git a/test/export/test_target_variable_trimming/expected_repo/.plzconfig b/test/export/test_target_variable_trimming/expected_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_target_variable_trimming/expected_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_target_variable_trimming/expected_repo/BUILD_FILE b/test/export/test_target_variable_trimming/expected_repo/BUILD_FILE
new file mode 100644
index 0000000000..e0835bd841
--- /dev/null
+++ b/test/export/test_target_variable_trimming/expected_repo/BUILD_FILE
@@ -0,0 +1,12 @@
+kept_var = filegroup(
+ name = "kept_var",
+ srcs = ["file1.txt"],
+)
+
+filegroup(
+ name = "used",
+ srcs = [
+ "file3.txt",
+ kept_var,
+ ],
+)
diff --git a/test/export/test_target_variable_trimming/expected_repo/file1.txt b/test/export/test_target_variable_trimming/expected_repo/file1.txt
new file mode 100644
index 0000000000..e2129701f1
--- /dev/null
+++ b/test/export/test_target_variable_trimming/expected_repo/file1.txt
@@ -0,0 +1 @@
+file1
diff --git a/test/export/test_target_variable_trimming/expected_repo/file3.txt b/test/export/test_target_variable_trimming/expected_repo/file3.txt
new file mode 100644
index 0000000000..7c8ac2f8d8
--- /dev/null
+++ b/test/export/test_target_variable_trimming/expected_repo/file3.txt
@@ -0,0 +1 @@
+file3
diff --git a/test/export/test_target_variable_trimming/source_repo/.plzconfig b/test/export/test_target_variable_trimming/source_repo/.plzconfig
new file mode 100644
index 0000000000..f8ba31854d
--- /dev/null
+++ b/test/export/test_target_variable_trimming/source_repo/.plzconfig
@@ -0,0 +1,2 @@
+[Parse]
+BuildFileName = BUILD_FILE
diff --git a/test/export/test_target_variable_trimming/source_repo/BUILD_FILE b/test/export/test_target_variable_trimming/source_repo/BUILD_FILE
new file mode 100644
index 0000000000..b011cb403d
--- /dev/null
+++ b/test/export/test_target_variable_trimming/source_repo/BUILD_FILE
@@ -0,0 +1,22 @@
+kept_var = filegroup(
+ name = "kept_var",
+ srcs = ["file1.txt"],
+)
+
+trimmed_var = filegroup(
+ name = "trimmed_var",
+ srcs = ["file2.txt"],
+)
+
+filegroup(
+ name = "used",
+ srcs = [
+ "file3.txt",
+ kept_var,
+ ],
+)
+
+filegroup(
+ name = "unused",
+ srcs = [trimmed_var],
+)
diff --git a/test/export/test_target_variable_trimming/source_repo/file1.txt b/test/export/test_target_variable_trimming/source_repo/file1.txt
new file mode 100644
index 0000000000..e2129701f1
--- /dev/null
+++ b/test/export/test_target_variable_trimming/source_repo/file1.txt
@@ -0,0 +1 @@
+file1
diff --git a/test/export/test_target_variable_trimming/source_repo/file2.txt b/test/export/test_target_variable_trimming/source_repo/file2.txt
new file mode 100644
index 0000000000..6c493ff740
--- /dev/null
+++ b/test/export/test_target_variable_trimming/source_repo/file2.txt
@@ -0,0 +1 @@
+file2
diff --git a/test/export/test_target_variable_trimming/source_repo/file3.txt b/test/export/test_target_variable_trimming/source_repo/file3.txt
new file mode 100644
index 0000000000..7c8ac2f8d8
--- /dev/null
+++ b/test/export/test_target_variable_trimming/source_repo/file3.txt
@@ -0,0 +1 @@
+file3