diff --git a/BUILD.bazel b/BUILD.bazel index 30ede7bbd6..933930100d 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -18,6 +18,7 @@ load("@rules_python//python:pip.bzl", "compile_pip_requirements") load("@rules_python_gazelle_plugin//manifest:defs.bzl", "gazelle_python_manifest") load("@rules_python_gazelle_plugin//modules_mapping:def.bzl", "modules_mapping") load("//bzl:rules.bzl", "bazel_lint") +load("//go/cmd/version_sync:go_version_sync.bzl", "go_version_sync") load("//js:rules.bzl", "copy_to_bin", "js_library") load("//ts:rules.bzl", "ts_config") @@ -111,29 +112,8 @@ bin.renovate_config_validator_test( npm_link_all_packages(name = "node_modules") -native_test( - name = "go_versions_synced", - size = "small", - src = "//go/cmd/version_sync", - out = "version_sync_test.o", - data = [ - "MODULE.bazel", - "go.mod", - ], -) - -sh_binary( - name = "go_versions_synced.fix", - srcs = ["fix_go_version_sync.sh"], - data = [ - "//go/cmd/version_sync", - ], - env = { - "VERSION_SYNC": "$(rlocationpath //go/cmd/version_sync)", - }, - deps = [ - "@bazel_tools//tools/bash/runfiles", - ], +go_version_sync( + name = "go_version_sync", ) js_library( @@ -246,6 +226,7 @@ py_venv( "//project/cultist/gen/testing", "//py", "//py/ci/postUpgrade:postUpgrade_bin", + "//py/copy_to_workspace:copy_to_workspace_bin", "//py/devtools", "//py/devtools/prep", "//py/devtools/prep:prep_bin", diff --git a/MODULE.bazel b/MODULE.bazel old mode 100644 new mode 100755 index 6373a5f65d..131d7d8661 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -128,8 +128,10 @@ bazel_dep(name = "bazel_features", version = "1.23.0") bazel_dep(name = "rules_go", version = "0.52.0", repo_name = "io_bazel_rules_go") bazel_dep(name = "gazelle", version = "0.41.0", repo_name = "bazel_gazelle") +GO_VERSION = "1.23.5" + go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk") -go_sdk.download(version = "1.23.5") +go_sdk.download(version = GO_VERSION) use_repo( go_sdk, "go_toolchains", @@ -149,6 +151,7 @@ use_repo( "com_github_aws_aws_sdk_go_v2_service_s3", "com_github_bazelbuild_bazel_watcher", "com_github_bazelbuild_buildtools", + "com_github_blang_semver_v4", "com_github_go_delve_delve", "com_github_golang_protobuf", "com_github_google_go_containerregistry", diff --git a/bzl/binary_with_args/BUILD.bazel b/bzl/binary_with_args/BUILD.bazel new file mode 100644 index 0000000000..116c96e56d --- /dev/null +++ b/bzl/binary_with_args/BUILD.bazel @@ -0,0 +1,14 @@ +load("//bzl:rules.bzl", "bazel_lint") + +exports_files( + ["binary_with_args.sh"], + visibility = ["//:__subpackages__"], +) + +bazel_lint( + name = "bazel_lint", + srcs = [ + "BUILD.bazel", + "binary_with_args.bzl", + ], +) diff --git a/bzl/binary_with_args/binary_with_args.bzl b/bzl/binary_with_args/binary_with_args.bzl new file mode 100644 index 0000000000..9c31eb0093 --- /dev/null +++ b/bzl/binary_with_args/binary_with_args.bzl @@ -0,0 +1,15 @@ +def binary_with_args(name, binary, args, data = [], **kwargs): + native.sh_binary( + name = name, + srcs = ["//bzl/binary_with_args:binary_with_args.sh"], + data = [ + binary, + ] + data, + args = [ + "$(rlocationpath " + binary + ")", + ] + args, + deps = [ + "@bazel_tools//tools/bash/runfiles", + ], + **kwargs + ) diff --git a/bzl/binary_with_args/binary_with_args.sh b/bzl/binary_with_args/binary_with_args.sh new file mode 100755 index 0000000000..218ec397ee --- /dev/null +++ b/bzl/binary_with_args/binary_with_args.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# --- begin runfiles.bash initialization v3 --- +# Copy-pasted from the Bazel Bash runfiles library v3. +set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash +# shellcheck disable=SC1090 +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e +# --- end runfiles.bash initialization v3 --- + +BINARY="$(rlocation $1)" +shift + +$BINARY $@ diff --git a/bzl/golden_test.bzl b/bzl/golden_test.bzl new file mode 100644 index 0000000000..83f7920f9a --- /dev/null +++ b/bzl/golden_test.bzl @@ -0,0 +1,25 @@ +load("@aspect_bazel_lib//lib:diff_test.bzl", "diff_test") +load("//py/copy_to_workspace:copy_to_workspace.bzl", "copy_to_workspace") + +def golden_test( + name, + src, + golden, # must be a file in this package. + **kwargs): + diff_test( + name = name, + file1 = src, + file2 = golden, + **kwargs + ) + + package = native.package_name() + + if package != "": + package += "/" + + copy_to_workspace( + name = name + ".fix", + src = golden, + dst = package + src, + ) diff --git a/go.mod b/go.mod old mode 100644 new mode 100755 index 00af8b4df8..7e99aa88e2 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/bazelbuild/bazel-watcher v0.25.3 github.com/bazelbuild/buildtools v0.0.0-20240918101019-be1c24cc9a44 github.com/bazelbuild/rules_go v0.52.0 + github.com/blang/semver/v4 v4.0.0 github.com/go-delve/delve v1.24.0 github.com/golang/protobuf v1.5.4 github.com/google/go-containerregistry v0.20.3 diff --git a/go.sum b/go.sum index ecbbd6c71e..fa580921e8 100644 --- a/go.sum +++ b/go.sum @@ -54,6 +54,8 @@ github.com/bazelbuild/rules_go v0.52.0/go.mod h1:M+YrupNArA7OiTlv++rFUgQ6Sm+ZXbQ github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beevik/etree v1.4.1 h1:PmQJDDYahBGNKDcpdX8uPy1xRCwoCGVUiW669MEirVI= github.com/beevik/etree v1.4.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= diff --git a/go/cmd/version_sync/BUILD.bazel b/go/cmd/version_sync/BUILD.bazel index c2f424e592..be0945cbbe 100644 --- a/go/cmd/version_sync/BUILD.bazel +++ b/go/cmd/version_sync/BUILD.bazel @@ -12,6 +12,7 @@ go_library( srcs = ["main.go"], importpath = "github.com/zemn-me/monorepo/go/cmd/version_sync", visibility = ["//visibility:private"], + deps = ["@com_github_blang_semver_v4//:semver"], ) bazel_lint( diff --git a/go/cmd/version_sync/go_version_sync.bzl b/go/cmd/version_sync/go_version_sync.bzl new file mode 100644 index 0000000000..77b9985b37 --- /dev/null +++ b/go/cmd/version_sync/go_version_sync.bzl @@ -0,0 +1,43 @@ +load("//bzl:golden_test.bzl", "golden_test") + + +def go_version_sync(name): + native.genrule( + name = name + "_gen_goldens", + tools = [ + "//go/cmd/version_sync" + ], + cmd_bash = """ +$(execpath //go/cmd/version_sync) \\ + -go-mod $(rootpath go.mod) \\ + -module-bazel $(rootpath MODULE.bazel) \\ + -output-go-mod $(location go.mod.golden) \\ + -output-module-bazel $(location MODULE.bazel.golden) \\ + -fix + """, + srcs = [ + "MODULE.bazel", + "go.mod" + ], + outs = ["go.mod.golden", "MODULE.bazel.golden"], + ) + + golden_test( + name = name + "_go_mod_test", + src = "go.mod", + golden = "go.mod.golden" + ) + + golden_test( + name = name + "_bazel_mod_test", + src = "MODULE.bazel", + golden = "MODULE.bazel.golden" + ) + + native.test_suite( + name = name, + tests = [ + name + "_go_mod_test", + name + "_bazel_mod_test" + ] + ) diff --git a/go/cmd/version_sync/main.go b/go/cmd/version_sync/main.go index f4d1297521..7a703ec036 100644 --- a/go/cmd/version_sync/main.go +++ b/go/cmd/version_sync/main.go @@ -1,240 +1,252 @@ -// cmd version_sync keeps go.mod and go_version.bzl in sync package main import ( "bufio" - "bytes" - "errors" "flag" "fmt" - "io" "os" "regexp" + "strings" + + "github.com/blang/semver/v4" ) var ( - fix bool - test bool - module_bazel string - go_module_file string + goModPath string + bazelPath string + fixDiscrepancy bool + outputGoMod string + outputBazel string ) -const example_line = `go_sdk.download(version = "1.22.2")` - func init() { - flag.BoolVar(&fix, "fix", false, "Whether to fix the bazel file if it is not in sync.") - flag.BoolVar(&test, "test", true, "Whether to return a non-zero status if not in sync.") - flag.StringVar(&module_bazel, "module_bazel", "MODULE.bazel", fmt.Sprintf("The go module file to update. Must have a line like %+q.", example_line)) - flag.StringVar(&go_module_file, "moduleFile", "go.mod", "The go.mod file to read from.") -} - -type FileLike interface { - io.Reader - io.Seeker - io.Writer - io.Closer - Truncate(size int64) error - Name() string + flag.StringVar(&goModPath, "go-mod", "go.mod", "Path to the go.mod file") + flag.StringVar(&bazelPath, "module-bazel", "MODULE.bazel", "Path to the MODULE.bazel file") + flag.StringVar(&outputGoMod, "output-go-mod", "", "Path to output the updated go.mod") + flag.StringVar(&outputBazel, "output-module-bazel", "", "Path to output the updated MODULE.bazel") + flag.BoolVar(&fixDiscrepancy, "fix", false, "Fix discrepancies instead of just reporting errors") } -type File struct { - FileLike -} - -func (v File) GetFirstSubmatch(re *regexp.Regexp) (start int, end int, err error) { - if _, err = v.Seek(0, io.SeekStart); err != nil { - return - } - versionIndex := re.FindReaderSubmatchIndex(bufio.NewReader(v)) - if len(versionIndex) < 2*1+2 { - err = errors.New("Could not find match") - return +func main() { + flag.Parse() + if err := Do(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) } - - start, end = versionIndex[2*1], versionIndex[2*1+2-1] - - return } -// FileFiddler, given a File, and a SegementMatcher, provides -// convenient tools for efficiently finding and replacing -// part of a file matching a regex. -type FileFiddler struct { - File - SegmentMatcher *regexp.Regexp - start *int - end *int - segment []byte -} +// Do orchestrates reading versions from go.mod and MODULE.bazel, compares them, +// and updates where needed if -fix is set. +func Do() error { + // 1. Read versions from go.mod (both 'go' and 'toolchain go') + goModVersion, toolchainVersion, err := readGoModVersions(goModPath) + if err != nil { + return fmt.Errorf("failed to read go.mod: %w", err) + } -func (f *FileFiddler) Offsets() (start int, end int, err error) { - if f.start == nil || f.end == nil { - f.start, f.end = new(int), new(int) - if *f.start, *f.end, err = f.GetFirstSubmatch(f.SegmentMatcher); err != nil { - f.start, f.end = nil, nil - err = fmt.Errorf("find %+q in %+q: %v", f.SegmentMatcher.String(), f.File.Name(), err) - return - } + // 2. Read version from MODULE.bazel + bazelGoVersion, err := readBazelGoVersion(bazelPath) + if err != nil { + return fmt.Errorf("failed to read MODULE.bazel: %w", err) } - return *f.start, *f.end, nil -} + // 3. Determine the highest version amongst what's found + latest, err := getLatestVersionOfAll([]string{goModVersion, bazelGoVersion, toolchainVersion}) + if err != nil { + return fmt.Errorf("failed to compute latest version: %w", err) + } -func (f *FileFiddler) ReadSegment() (segment []byte, err error) { - if f.segment != nil { - return f.segment, nil + // If we're just checking consistency, fail if there's a mismatch + if !fixDiscrepancy { + // If any two differ, we consider that inconsistent + if (goModVersion != "" && goModVersion != latest) || + (toolchainVersion != "" && toolchainVersion != latest) || + bazelGoVersion != latest { + return fmt.Errorf("versions are inconsistent; run with -fix to synchronise to %s", latest) + } + return nil } - var start int - var end int - if start, end, err = f.Offsets(); err != nil { - err = fmt.Errorf("read matched segment: %v", err) - return + // 4. Fix go.mod lines if needed + if goModVersion != latest || toolchainVersion != latest { + fmt.Printf("Updating go.mod to use %s (was go=%s, toolchain go=%s)\n", + latest, safeStr(goModVersion), safeStr(toolchainVersion)) + if err := rewriteGoMod(goModPath, outputGoMod, latest); err != nil { + return fmt.Errorf("failed to update go.mod: %w", err) + } + } else if outputGoMod != "" { + // If no change but user wants a separate output file, just copy + if err := copyFile(goModPath, outputGoMod); err != nil { + return fmt.Errorf("failed to copy go.mod to output: %w", err) + } } - f.Seek(int64(start), io.SeekStart) - segment = make([]byte, end-start) - if _, err = f.Read(segment); err != nil { - err = fmt.Errorf("read from byte %d to %d of %+q:", start, end, f.File.Name()) - return + // 5. Fix MODULE.bazel if needed + if bazelGoVersion != latest { + fmt.Printf("Updating MODULE.bazel from %s to %s\n", bazelGoVersion, latest) + if err := rewriteBazelGoVersion(bazelPath, outputBazel, latest); err != nil { + return fmt.Errorf("failed to update MODULE.bazel: %w", err) + } + } else if outputBazel != "" { + if err := copyFile(bazelPath, outputBazel); err != nil { + return fmt.Errorf("failed to copy MODULE.bazel to output: %w", err) + } } - return + return nil } -func (f FileFiddler) OverwriteSegment(n []byte) (err error) { - var start int - if start, _, err = f.Offsets(); err != nil { - return - } - // buffer the whole file after the end of the match - var b bytes.Buffer - if _, err = io.Copy(&b, f); err != nil { - return - } - // truncate the file at the beginning of the match - if err = f.Truncate(int64(start)); err != nil { - return +// readGoModVersions returns two strings: (goVersion, toolchainGoVersion). +// If a line "go X" is found, that becomes goVersion. +// If a line "toolchain go Y" is found, that becomes toolchainGoVersion. +func readGoModVersions(path string) (string, string, error) { + file, err := os.Open(path) + if err != nil { + return "", "", fmt.Errorf("unable to open file %s: %w", path, err) } + defer file.Close() + + var ( + goVersion string + toolchainGoVersion string + ) - // append the new segment - if _, err = f.Seek(int64(start), io.SeekStart); err != nil { - return + scanner := bufio.NewScanner(file) + reGo := regexp.MustCompile(`^go\s+(\S+)$`) + reToolchain := regexp.MustCompile(`^toolchain\s+go\s+(\S+)$`) + + for scanner.Scan() { + line := scanner.Text() + if matches := reGo.FindStringSubmatch(line); len(matches) == 2 { + goVersion = matches[1] + } else if matches := reToolchain.FindStringSubmatch(line); len(matches) == 2 { + toolchainGoVersion = matches[1] + } } - if _, err = io.Copy(f, io.MultiReader(bytes.NewReader(n), &b)); err != nil { - return + if err := scanner.Err(); err != nil { + return "", "", fmt.Errorf("error reading file %s: %w", path, err) } - return + // It's possible none or only one of them was found. + // We'll allow empty returns to indicate "not found". + return goVersion, toolchainGoVersion, nil } -var ReVersionFileVersion = regexp.MustCompile(`go_sdk.download\s*\(\s*version\s*=\s*"([^"]*)"\s*\)\n`) +// rewriteGoMod updates both `go ` and `toolchain go ` lines (if present). +func rewriteGoMod(srcPath, outputPath, newVersion string) error { + contents, err := os.ReadFile(srcPath) + if err != nil { + return fmt.Errorf("unable to read file %s: %w", srcPath, err) + } + lines := strings.Split(string(contents), "\n") -type VersionFile struct { - FileFiddler -} + reGo := regexp.MustCompile(`^go\s+(\S+)$`) + reToolchain := regexp.MustCompile(`^toolchain\s+go\s+(\S+)$`) -func (v *VersionFile) LazyInit() { - if v.FileFiddler.SegmentMatcher == nil { - v.FileFiddler.SegmentMatcher = ReVersionFileVersion + for i, line := range lines { + switch { + case reGo.MatchString(line): + lines[i] = "go " + newVersion + case reToolchain.MatchString(line): + lines[i] = "toolchain go " + newVersion + } } -} -func (v *VersionFile) Version() (version []byte, err error) { - v.LazyInit() - version, err = v.ReadSegment() - if err != nil { - err = fmt.Errorf("While getting version from %+q: %v", v.File.Name(), err) + destPath := srcPath + if outputPath != "" { + destPath = outputPath } - return -} - -func (v *VersionFile) SetVersion(b []byte) (err error) { - v.LazyInit() - if err = v.OverwriteSegment(b); err != nil { - err = fmt.Errorf("While setting new version (%+q) in %+q: %v", b, v.File.Name(), err) + if err := os.WriteFile(destPath, []byte(strings.Join(lines, "\n")), 0644); err != nil { + return fmt.Errorf("unable to write updated go.mod to %s: %w", destPath, err) } - return -} - -var ReModuleFileVersion = regexp.MustCompile(`go ([^\s]+)\n`) - -type ModuleFile struct { - FileFiddler + return nil } -func (v *ModuleFile) LazyInit() { - if v.FileFiddler.SegmentMatcher == nil { - v.FileFiddler.SegmentMatcher = ReModuleFileVersion +// readBazelGoVersion scans MODULE.bazel for a line matching `GO_VERSION = ""`. +func readBazelGoVersion(path string) (string, error) { + file, err := os.Open(path) + if err != nil { + return "", fmt.Errorf("unable to open file %s: %w", path, err) } -} - -func (v *ModuleFile) Version() (version []byte, err error) { - v.LazyInit() - return v.ReadSegment() -} + defer file.Close() -func (v *ModuleFile) SetVersion(b []byte) (err error) { - v.LazyInit() - return v.OverwriteSegment(b) -} + scanner := bufio.NewScanner(file) + re := regexp.MustCompile(`^\s*GO_VERSION\s*=\s*"([^"]+)"`) -func do() (err error) { - var fileMode int = os.O_RDONLY - if fix { - fileMode = os.O_RDWR + for scanner.Scan() { + line := scanner.Text() + if matches := re.FindStringSubmatch(line); len(matches) == 2 { + return matches[1], nil + } } - vff, err := os.OpenFile(module_bazel, fileMode, 0o777) - if err != nil { - err = fmt.Errorf("Opening version file %+q: %v", module_bazel, err) - return + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("error reading file %s: %w", path, err) } + return "", fmt.Errorf("no GO_VERSION found in %s", path) +} - versionFile := VersionFile{FileFiddler: FileFiddler{File: File{FileLike: vff}}} - defer versionFile.Close() - - modf, err := os.OpenFile(go_module_file, fileMode, 0o777) +// rewriteBazelGoVersion reads MODULE.bazel, updates `GO_VERSION = ""`. +func rewriteBazelGoVersion(srcPath, outputPath, newVersion string) error { + contents, err := os.ReadFile(srcPath) if err != nil { - err = fmt.Errorf("Opening go module file %+q: %v", go_module_file, err) - return + return fmt.Errorf("unable to read file %s: %w", srcPath, err) } + lines := strings.Split(string(contents), "\n") - moduleFile := ModuleFile{FileFiddler: FileFiddler{File: File{FileLike: modf}}} - defer moduleFile.Close() + re := regexp.MustCompile(`^(\s*)GO_VERSION\s*=\s*"([^"]+)"`) + for i, line := range lines { + if matches := re.FindStringSubmatch(line); len(matches) == 3 { + prefix := matches[1] + lines[i] = fmt.Sprintf(`%sGO_VERSION = "%s"`, prefix, newVersion) + break + } + } - versionFileVersion, err := versionFile.Version() - if err != nil { - err = fmt.Errorf("While getting version from bazel module: %v", err) - return + destPath := srcPath + if outputPath != "" { + destPath = outputPath + } + if err := os.WriteFile(destPath, []byte(strings.Join(lines, "\n")), 0644); err != nil { + return fmt.Errorf("unable to write updated MODULE.bazel to %s: %w", destPath, err) } + return nil +} - moduleFileVersion, err := moduleFile.Version() +// copyFile copies contents from src to dst unmodified. +func copyFile(src, dst string) error { + data, err := os.ReadFile(src) if err != nil { - err = fmt.Errorf("While getting version from go module: %v", err) - return + return err } + return os.WriteFile(dst, data, 0644) +} - inSync := bytes.Equal(versionFileVersion, moduleFileVersion) - - if fix && !inSync { - err = versionFile.OverwriteSegment(moduleFileVersion) +// getLatestVersionOfAll finds the highest semver among a list of versions (some may be empty). +func getLatestVersionOfAll(versions []string) (string, error) { + var maxVer *semver.Version + for _, v := range versions { + if v == "" { + continue + } + parsed, err := semver.ParseTolerant(v) if err != nil { - err = fmt.Errorf("While setting bazel module version to +%q: %v", moduleFileVersion, err) - return + return "", fmt.Errorf("invalid semver version: %s (%w)", v, err) + } + if maxVer == nil || parsed.GT(*maxVer) { + maxVer = &parsed } } - - if test && !inSync { - return fmt.Errorf("%+q and %+q are not in sync. %+q has: %+q; %+q has: %+q.", module_bazel, go_module_file, module_bazel, versionFileVersion, go_module_file, moduleFileVersion) + if maxVer == nil { + return "", fmt.Errorf("no valid version discovered") } - - return + return maxVer.String(), nil } -func main() { - flag.Parse() - if err := do(); err != nil { - panic(err) +// safeStr helps printing empty-version placeholders gracefully. +func safeStr(s string) string { + if s == "" { + return "N/A" } + return s } diff --git a/py/copy_to_workspace/BUILD.bazel b/py/copy_to_workspace/BUILD.bazel new file mode 100644 index 0000000000..f90624a1d8 --- /dev/null +++ b/py/copy_to_workspace/BUILD.bazel @@ -0,0 +1,18 @@ +load("//bzl:rules.bzl", "bazel_lint") +load("//py:rules.bzl", "py_binary") + +py_binary( + name = "copy_to_workspace_bin", + srcs = ["__main__.py"], + main = "__main__.py", + visibility = ["//:__subpackages__"], + deps = ["@rules_python//python/runfiles"], +) + +bazel_lint( + name = "bazel_lint", + srcs = [ + "BUILD.bazel", + "copy_to_workspace.bzl", + ], +) diff --git a/py/copy_to_workspace/__main__.py b/py/copy_to_workspace/__main__.py new file mode 100644 index 0000000000..060772e0d3 --- /dev/null +++ b/py/copy_to_workspace/__main__.py @@ -0,0 +1,65 @@ +""" +Copies a file from bazel's output tree $(rlocationpath //something), to +the live workspace the dev is working in. + +This allows us to have an output of a bazel action copied into the source code +(e.g. for goldens). +""" +import os +import shutil +import sys +from python.runfiles import runfiles + +def copy_file(rlocation_path, workspace_subpath): + """ + Copies a file from an rlocation path to a specified path under $BUILD_WORKSPACE_DIRECTORY. + + Args: + rlocation_path (str): The source file path (rlocation). + workspace_subpath (str): The relative path under $BUILD_WORKSPACE_DIRECTORY to copy the file to. + + Raises: + FileNotFoundError: If the source file does not exist. + OSError: If there is an issue with file copying. + """ + r = runfiles.Create() + if r is None: + raise Exception("Unable to build runfiles. Are you in Bazel?") + + source_path = r.Rlocation(rlocation_path) + + if source_path is None: + raise Exception("Unable to locate runfile: " + rlocation_path) + + build_workspace_directory = os.environ.get("BUILD_WORKSPACE_DIRECTORY") + if not build_workspace_directory: + raise EnvironmentError("$BUILD_WORKSPACE_DIRECTORY is not set") + + destination_path = os.path.join(build_workspace_directory, workspace_subpath) + + if not os.path.exists(source_path): + raise FileNotFoundError(f"Source file not found: {source_path}") + + # Ensure the destination directory exists + os.makedirs(os.path.dirname(destination_path), exist_ok=True) + + # Copy the file + try: + shutil.copy2(source_path, destination_path) + print(f"File copied to: {destination_path}") + except OSError as e: + raise OSError(f"Failed to copy file: {e}") + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python copy_rlocation.py ") + sys.exit(1) + + rlocation_path = sys.argv[1] + workspace_subpath = sys.argv[2] + + try: + copy_file(rlocation_path, workspace_subpath) + except Exception as e: + print(f"Error: {e}") + sys.exit(1) diff --git a/py/copy_to_workspace/copy_to_workspace.bzl b/py/copy_to_workspace/copy_to_workspace.bzl new file mode 100644 index 0000000000..a393cd0fc9 --- /dev/null +++ b/py/copy_to_workspace/copy_to_workspace.bzl @@ -0,0 +1,17 @@ +load("//bzl/binary_with_args:binary_with_args.bzl", "binary_with_args") + +def copy_to_workspace( + name, + src, # bazel output to copy + dst): # where to put it in the repo + binary_with_args( + name = name, + binary = "//py/copy_to_workspace:copy_to_workspace_bin", + args = [ + "$(rlocationpath " + src + ")", + dst, + ], + data = [ + src, + ], + )