Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/BUILD.plz
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ go_binary(
"//src/version",
"//src/watch",
],
strip = False, # Example change
)

# This is handy for things like plz plz --repo_root
Expand Down
60 changes: 60 additions & 0 deletions src/core/build_target.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@ type BuildTarget struct {
// If true, the interactive progress display will try to infer the target's progress
// via some heuristics on its output.
showProgress atomic.Bool `name:"progress"`
// Metadata collected during parsing and interpreter steps
ParseMetadata ParseMetadata
}

// ExpectedBuildMetadataVersionTag is the version tag that the current Please version expects. If this doesn't match
Expand Down Expand Up @@ -296,6 +298,45 @@ type BuildMetadata struct {
VersionTag int
}

type ParseMetadata struct {
// A call stack from the interpret step that generated this target.
CallStack CallStack
}

type CallStack []CallFrame

func (cs *CallStack) Push(item CallFrame) {
*cs = append(*cs, item)
}

func (cs *CallStack) Pop() CallFrame {
if len(*cs) == 0 {
var zero CallFrame
return zero
}
item := (*cs)[len(*cs)-1]
*cs = (*cs)[:len(*cs)-1]
return item
}

type CallFrame struct {
MethodName string
Label BuildLabel
Statement BuildStatement
}

type BuildStatement struct {
Filename string
Start int
End int
}

func (s BuildStatement) Len() int {
return s.End - s.Start
}



// A PreBuildFunction is a type that allows hooking a pre-build callback.
type PreBuildFunction interface {
fmt.Stringer
Expand Down Expand Up @@ -1971,6 +2012,25 @@ func (target *BuildTarget) NeedCoverage(state *BuildState) bool {
return state.NeedCoverage && !target.Test.NoOutput && !target.Test.NoCoverage && !target.HasAnyLabel(state.Config.Test.DisableCoverage)
}

// RelatedTargets returns all the targets in the package that originate from the same Build Statement.
func (target *BuildTarget) RelatedTargets(graph *BuildGraph) BuildTargets {
relatedTargets := make(BuildTargets, 0)
pkg := graph.PackageOrDie(target.Label)
for _, otherTarget := range pkg.AllTargets() {
if otherTarget.isRelated(target) {
relatedTargets = append(relatedTargets, otherTarget)
}
}
return relatedTargets
}

func (target *BuildTarget) isRelated(other *BuildTarget) bool {
if len(target.ParseMetadata.CallStack) == 0 || len(other.ParseMetadata.CallStack) == 0 {
return false
}
return target.ParseMetadata.CallStack[0].Statement == other.ParseMetadata.CallStack[0].Statement
}

// Parent finds the parent of a build target, or nil if the target is parentless.
// Note that this is a fairly informal relationship; we identify it by labels with the convention of
// a leading _ and trailing hashtag on child rules, rather than storing pointers between them in the graph.
Expand Down
1 change: 0 additions & 1 deletion src/export/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ go_library(
"//src/cli/logging",
"//src/core",
"//src/fs",
"//src/gc",
"//src/parse",
],
)
153 changes: 106 additions & 47 deletions src/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
package export

import (
"fmt"
iofs "io/fs"
"os"
"path/filepath"
"slices"
"strings"

"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"
)

Expand All @@ -22,19 +24,21 @@ type export struct {
targetDir string
noTrim bool

exportedTargets map[core.BuildLabel]bool
exportedPackages map[string]bool
exportedTargets map[core.BuildLabel]bool
exportedPackages map[string]bool
selectedStatements map[string]map[core.BuildStatement]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{},
state: state,
noTrim: noTrim,
targetDir: dir,
exportedPackages: map[string]bool{},
exportedTargets: map[core.BuildLabel]bool{},
selectedStatements: map[string]map[core.BuildStatement]bool{},
}

if err := os.MkdirAll(dir, fs.DirPermissions); err != nil {
Expand All @@ -47,48 +51,20 @@ func ToDir(state *core.BuildState, dir string, noTrim bool, targets []core.Build
e.export(state.Graph.TargetOrDie(includeLabel))
}
}

log.Warningf("Exporting selected targets: %v", targets)
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
}

e.writeBuildStatements()

// 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)
}
}

if noTrim {
return // We have already exported the whole directory
}

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)
}
}
if err := gc.RewriteFile(state, dest, victims); err != nil {
log.Fatalf("Failed to rewrite BUILD file: %s\n", err)
}
}
}

func (e *export) exportPlzConf() {
Expand All @@ -99,7 +75,7 @@ func (e *export) exportPlzConf() {
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)
log.Fatalf("failed to remove .plzconfig 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)
Expand All @@ -116,6 +92,7 @@ func (e *export) exportSources(target *core.BuildTarget) {
if err := fs.RecursiveCopy(p, filepath.Join(e.targetDir, p), 0); err != nil {
log.Fatalf("Error copying file: %s\n", err)
}
log.Warning("Writing source file: %s", p)
}
}
}
Expand All @@ -129,8 +106,8 @@ var ignoreDirectories = map[string]bool{
".hg": true,
}

// exportPackage exports the package BUILD file containing the given target and all sources
func (e *export) exportPackage(target *core.BuildTarget) {
// exportEntirePackage exports the package BUILD file containing the given target and all sources
func (e *export) exportEntirePackage(target *core.BuildTarget) {
pkgName := target.Label.PackageName
if pkgName == parse.InternalPackageName {
return
Expand Down Expand Up @@ -169,31 +146,113 @@ func (e *export) exportPackage(target *core.BuildTarget) {
}
}

// selectBuildStatements exports BUILD statements that generate the build target.
func (e *export) selectBuildStatements(target *core.BuildTarget) {
if target.Label.PackageName == parse.InternalPackageName {
return
}

log.Infof("Selecting Build stmts of %s with callstack:\n", target.Label.String())
for _, frame := range target.ParseMetadata.CallStack {
log.Infof("\t%v", frame)

if frame.Statement.Filename != "" && !strings.HasPrefix(frame.Statement.Filename, core.OutDir) {
if _, ok := e.selectedStatements[frame.Statement.Filename]; !ok {
e.selectedStatements[frame.Statement.Filename] = map[core.BuildStatement]bool{}
}
e.selectedStatements[frame.Statement.Filename][frame.Statement] = true
}
if frame.Statement.Filename == "" {
log.Warning("Package without filename %v", frame)
}
}
}

// export implements the logic of ToDir, but prevents repeating targets.
func (e *export) export(target *core.BuildTarget) {
if e.exportedTargets[target.Label] {
return
}
log.Warningf("Exporting %v.\n", target.Label)
e.exportedTargets[target.Label] = true

// 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 {
log.Warningf("Subrepo: %v", target.Subrepo.Target)
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)
e.exportEntirePackage(target)
} else {
e.selectBuildStatements(target)
e.exportSources(target)
}

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)

for _, otherTarget := range target.RelatedTargets(e.state.Graph) {
log.Warningf("Exporting Other %s", otherTarget)
e.export(otherTarget)
}
}

// writeBuildStatements writes the BUILD file statements to the export directory.
func (e *export) writeBuildStatements() {
log.Warningf("Selected Statements: %v", e.selectedStatements)

for filename, stmtMap := range e.selectedStatements {
stmts := make([]core.BuildStatement, 0, len(stmtMap))
for stmt := range stmtMap {
stmts = append(stmts, stmt)
}
// Sort statements by position to keep them in order
slices.SortFunc(stmts, func(a, b core.BuildStatement) int {
return a.Start - b.Start
})

e.writeBuildFile(filename, stmts)
}
}

func (e *export) writeBuildFile(filename string, stmts []core.BuildStatement) {
log.Warningf("Writing file: %s", filename)
if err := fs.EnsureDir(filepath.Join(e.targetDir, filename)); err != nil {
log.Fatalf("failed to create directory for %s: %v", filename, err)
}
fw, err := os.Create(filepath.Join(e.targetDir, filename))
if err != nil {
log.Fatalf("failed to create BUILD file %s: %v", filename, err)
}
defer fw.Close()

fr, err := os.Open(filename)
if err != nil {
// TODO ensure only visiting correct files and move Warn to Fatal
log.Warningf("failed to open file %s: %v", filename, err)
return
}
defer fr.Close()

for _, s := range stmts {
buff := make([]byte, s.Len())
_, err := fr.ReadAt(buff, int64(s.Start))
if err != nil {
log.Fatalf("failed to read BUILD file %s: %v", filename, err)
}

if _, err := fw.Write(buff); err != nil {
log.Fatalf("failed to write statement to %s: %v", filename, err)
}
if _, err := fmt.Fprintf(fw, "\n#%+v\n\n", s); err != nil {
log.Fatalf("failed to write newline to %s: %v", filename, err)
}
}
}

Expand Down
Loading