Skip to content

Commit

Permalink
runfiles: port phst/runfiles to rules_go
Browse files Browse the repository at this point in the history
Today rules_go provide //go/tools/bazel as the canonical runfiles
library for binaries to be executed with `bazel test` and `bazel run`.

However, the current implementation pre-date the recent changes in
Bazel's upstream.  Since then, all of the native runfiles library of
Bash, Java, CPP, Python have been refactored to follow a certain
convention in locating files. (1)

Although these are subjected to change with the incoming BzlMod feature,
it would be easier to maintain if we can keep rules_go's runfiles
library implementation aligned with native languages' implementation.

Today, it seems like https://github.com/phst/runfiles implemented
exactly that.  So with @fmeum suggestion and @phst permission (2), let's
port the newer, more accurate implementation to rules_go.

Future refactoring will mark the current exported APIs in
//go/tools/bazel as deprecated and/or swapping out the old implementation
underneath to use this newer package.

Changes in this PR included:
- Copy paste repository over
- Removal of .git and .gitignore and .githooks dir
- Removal of repository specific files: README, WORKSPACE
- Rename BUILD to BUILD.bazel
- Rename import path for both go and BUILD files
- Run gazelle over the packages
- Adjusted test cases to reflect new package paths
- Removed godoc related to installation instruction
- Fixed test to handle window path separator

(1): https://docs.google.com/document/d/e/2PACX-1vSDIrFnFvEYhKsCMdGdD40wZRBX3m3aZ5HhVj4CtHPmiXKDCxioTUbYsDydjKtFDAzER5eg7OjJWs3V/pub
(2): phst/runfiles#3 (comment)
  • Loading branch information
sluongng committed Jun 17, 2022
1 parent 1ceb5a7 commit f4352e7
Show file tree
Hide file tree
Showing 11 changed files with 802 additions and 0 deletions.
53 changes: 53 additions & 0 deletions go/tools/bazel/runfiles/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "runfiles",
srcs = [
"directory.go",
"fs.go",
"global.go",
"manifest.go",
"runfiles.go",
],
importpath = "github.com/bazelbuild/rules_go/go/tools/bazel/runfiles",
visibility = ["//visibility:public"],
)

go_test(
name = "runfiles_test",
srcs = [
"fs_test.go",
"runfiles_test.go",
],
data = [
"test.txt",
"//go/tools/bazel/runfiles/testprog",
],
rundir = ".",
deps = [":runfiles"],
)

exports_files(
["test.txt"],
visibility = ["//go/tools/bazel/runfiles/testprog:__pkg__"],
)

alias(
name = "go_default_library",
actual = ":runfiles",
visibility = ["//visibility:public"],
)
32 changes: 32 additions & 0 deletions go/tools/bazel/runfiles/directory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2020, 2021, 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package runfiles

import "path/filepath"

// Directory specifies the location of the runfiles directory. You can pass
// this as an option to New. If unset or empty, use the value of the
// environmental variable RUNFILES_DIR.
type Directory string

func (d Directory) new() *Runfiles {
return &Runfiles{d, directoryVar + "=" + string(d)}
}

func (d Directory) path(s string) (string, error) {
return filepath.Join(string(d), filepath.FromSlash(s)), nil
}

const directoryVar = "RUNFILES_DIR"
98 changes: 98 additions & 0 deletions go/tools/bazel/runfiles/fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2021, 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// +build go1.16

package runfiles

import (
"errors"
"io"
"io/fs"
"os"
"time"
)

// Open implements fs.FS.Open.
func (r *Runfiles) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
}
p, err := r.Path(name)
if errors.Is(err, ErrEmpty) {
return emptyFile(name), nil
}
if err != nil {
return nil, pathError("open", name, err)
}
return os.Open(p)
}

// Stat implements fs.StatFS.Stat.
func (r *Runfiles) Stat(name string) (fs.FileInfo, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrInvalid}
}
p, err := r.Path(name)
if errors.Is(err, ErrEmpty) {
return emptyFileInfo(name), nil
}
if err != nil {
return nil, pathError("stat", name, err)
}
return os.Stat(p)
}

// ReadFile implements fs.ReadFileFS.ReadFile.
func (r *Runfiles) ReadFile(name string) ([]byte, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
}
p, err := r.Path(name)
if errors.Is(err, ErrEmpty) {
return nil, nil
}
if err != nil {
return nil, pathError("open", name, err)
}
return os.ReadFile(p)
}

type emptyFile string

func (f emptyFile) Stat() (fs.FileInfo, error) { return emptyFileInfo(f), nil }
func (f emptyFile) Read([]byte) (int, error) { return 0, io.EOF }
func (emptyFile) Close() error { return nil }

type emptyFileInfo string

func (i emptyFileInfo) Name() string { return string(i) }
func (emptyFileInfo) Size() int64 { return 0 }
func (emptyFileInfo) Mode() fs.FileMode { return 0444 }
func (emptyFileInfo) ModTime() time.Time { return time.Time{} }
func (emptyFileInfo) IsDir() bool { return false }
func (emptyFileInfo) Sys() interface{} { return nil }

func pathError(op, name string, err error) error {
if err == nil {
return nil
}
var rerr Error
if errors.As(err, &rerr) {
// Unwrap the error because we don’t need the failing name
// twice.
return &fs.PathError{Op: op, Path: rerr.Name, Err: rerr.Err}
}
return &fs.PathError{Op: op, Path: name, Err: err}
}
96 changes: 96 additions & 0 deletions go/tools/bazel/runfiles/fs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build go1.16
// +build go1.16

package runfiles_test

import (
"io"
"io/fs"
"os"
"path/filepath"
"testing"
"testing/fstest"

"github.com/bazelbuild/rules_go/go/tools/bazel/runfiles"
)

func TestFS(t *testing.T) {
fsys, err := runfiles.New()
if err != nil {
t.Fatal(err)
}
// Ensure that the Runfiles object implements FS interfaces.
var _ fs.FS = fsys
var _ fs.StatFS = fsys
var _ fs.ReadFileFS = fsys
if err := fstest.TestFS(fsys, "io_bazel_rules_go/go/tools/bazel/runfiles/test.txt", "io_bazel_rules_go/go/tools/bazel/runfiles/testprog/testprog"); err != nil {
t.Error(err)
}
}

func TestFS_empty(t *testing.T) {
dir := t.TempDir()
manifest := filepath.Join(dir, "manifest")
if err := os.WriteFile(manifest, []byte("__init__.py \n"), 0600); err != nil {
t.Fatal(err)
}
fsys, err := runfiles.New(runfiles.ManifestFile(manifest), runfiles.ProgramName("/invalid"), runfiles.Directory("/invalid"))
if err != nil {
t.Fatal(err)
}
t.Run("Open", func(t *testing.T) {
fd, err := fsys.Open("__init__.py")
if err != nil {
t.Fatal(err)
}
defer fd.Close()
got, err := io.ReadAll(fd)
if err != nil {
t.Error(err)
}
if len(got) != 0 {
t.Errorf("got nonempty contents: %q", got)
}
})
t.Run("Stat", func(t *testing.T) {
got, err := fsys.Stat("__init__.py")
if err != nil {
t.Fatal(err)
}
if got.Name() != "__init__.py" {
t.Errorf("Name: got %q, want %q", got.Name(), "__init__.py")
}
if got.Size() != 0 {
t.Errorf("Size: got %d, want %d", got.Size(), 0)
}
if !got.Mode().IsRegular() {
t.Errorf("IsRegular: got %v, want %v", got.Mode().IsRegular(), true)
}
if got.IsDir() {
t.Errorf("IsDir: got %v, want %v", got.IsDir(), false)
}
})
t.Run("ReadFile", func(t *testing.T) {
got, err := fsys.ReadFile("__init__.py")
if err != nil {
t.Error(err)
}
if len(got) != 0 {
t.Errorf("got nonempty contents: %q", got)
}
})
}
60 changes: 60 additions & 0 deletions go/tools/bazel/runfiles/global.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2020, 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package runfiles

import "sync"

// Path returns the absolute path name of a runfile. The runfile name must be
// a relative path, using the slash (not backslash) as directory separator. If
// the runfiles manifest maps s to an empty name (indicating an empty runfile
// not present in the filesystem), Path returns an error that wraps ErrEmpty.
func Path(s string) (string, error) {
r, err := g.get()
if err != nil {
return "", err
}
return r.Path(s)
}

// Env returns additional environmental variables to pass to subprocesses.
// Each element is of the form “key=value”. Pass these variables to
// Bazel-built binaries so they can find their runfiles as well. See the
// Runfiles example for an illustration of this.
//
// The return value is a newly-allocated slice; you can modify it at will.
func Env() ([]string, error) {
r, err := g.get()
if err != nil {
return nil, err
}
return r.Env(), nil
}

type global struct {
once sync.Once
r *Runfiles
err error
}

func (g *global) get() (*Runfiles, error) {
g.once.Do(g.init)
return g.r, g.err
}

func (g *global) init() {
g.r, g.err = New()
}

var g global
Loading

0 comments on commit f4352e7

Please sign in to comment.