Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

runfiles: port phst/runfiles to rules_go #3205

Merged
merged 5 commits into from
Nov 6, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
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",
"@bazel_tools//tools/bash/runfiles",
],
deps = [":runfiles"],
)

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

alias(
name = "go_default_library",
actual = ":runfiles",
visibility = ["//visibility:public"],
)
30 changes: 30 additions & 0 deletions go/tools/bazel/runfiles/directory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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
}
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}
}
121 changes: 121 additions & 0 deletions go/tools/bazel/runfiles/fs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// 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"
"runtime"
"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 runtime.GOOS == "windows" {
// Currently the result of
//
// fsys.Path("io_bazel_rules_go/go/tools/bazel/runfiles/test.txt")
// fsys.Path("bazel_tools/tools/bash/runfiles/runfiles.bash")
// fsys.Path("io_bazel_rules_go/go/tools/bazel/runfiles/testprog/testprog")
//
// would be a full path like these
//
// C:\b\bk-windows-1z0z\bazel\rules-go-golang\go\tools\bazel\runfiles\test.txt
// C:\b\zslxztin\external\bazel_tools\tools\bash\runfiles\runfiles.bash
// C:\b\pm4ep4b2\execroot\io_bazel_rules_go\bazel-out\x64_windows-fastbuild\bin\go\tools\bazel\runfiles\testprog\testprog
//
// Which does not follow any particular patter / rules.
// This makes it very hard to define what we are looking for on Windows.
// So let's skip this for now.
return
}

expected1 := "io_bazel_rules_go/go/tools/bazel/runfiles/test.txt"
expected2 := "io_bazel_rules_go/go/tools/bazel/runfiles/testprog/testprog_/testprog"
expected3 := "bazel_tools/tools/bash/runfiles/runfiles.bash"
if err := fstest.TestFS(fsys, expected1, expected2, expected3); 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"), 0o600); 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
fmeum marked this conversation as resolved.
Show resolved Hide resolved
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