From fd8c0415054c923e62000417121dfd640c3cc14e Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Mon, 5 Dec 2022 23:40:26 +0100 Subject: [PATCH] Make Go runfiles library repo mapping aware (#3347) --- .bazelci/presubmit.yml | 2 +- go/BUILD.bazel | 1 + go/runfiles/BUILD.bazel | 7 ++ go/runfiles/directory.go | 10 +- go/runfiles/global.go | 43 ++++++- go/runfiles/manifest.go | 11 +- go/runfiles/runfiles.go | 116 ++++++++++++++++-- tests/bcr/.bazelversion | 2 +- tests/bcr/MODULE.bazel | 5 + tests/bcr/other_module/BUILD.bazel | 1 + tests/bcr/other_module/MODULE.bazel | 3 + tests/bcr/other_module/WORKSPACE | 0 tests/bcr/other_module/bar.txt | 1 + tests/bcr/runfiles/BUILD.bazel | 9 ++ tests/bcr/runfiles/runfiles_test.go | 67 ++++++++++ tests/runfiles/BUILD.bazel | 6 + tests/runfiles/runfiles_bazel_test.go | 168 ++++++++++++++++++++++++++ 17 files changed, 431 insertions(+), 21 deletions(-) create mode 100644 tests/bcr/other_module/BUILD.bazel create mode 100644 tests/bcr/other_module/MODULE.bazel create mode 100644 tests/bcr/other_module/WORKSPACE create mode 100644 tests/bcr/other_module/bar.txt create mode 100644 tests/bcr/runfiles/BUILD.bazel create mode 100644 tests/bcr/runfiles/runfiles_test.go create mode 100644 tests/runfiles/runfiles_bazel_test.go diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index ea6fad23f7..2f59d2b27a 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -24,7 +24,7 @@ tasks: ubuntu2004_bcr_tests: name: BCR test module platform: ubuntu2004 - bazel: b12f3a93a55019276879bd2d3edbd201c913675a + bazel: 6.0.0rc2 working_directory: tests/bcr build_flags: - "--allow_yanked_versions=all" diff --git a/go/BUILD.bazel b/go/BUILD.bazel index fbe073e73d..08c119f491 100644 --- a/go/BUILD.bazel +++ b/go/BUILD.bazel @@ -7,6 +7,7 @@ filegroup( "//go/config:all_files", "//go/constraints/amd64:all_files", "//go/platform:all_files", + "//go/runfiles:all_files", "//go/toolchain:all_files", "//go/tools:all_files", "//go/private:all_files", diff --git a/go/runfiles/BUILD.bazel b/go/runfiles/BUILD.bazel index 67dedc9c79..62875bd4a4 100644 --- a/go/runfiles/BUILD.bazel +++ b/go/runfiles/BUILD.bazel @@ -32,3 +32,10 @@ alias( actual = ":runfiles", visibility = ["//visibility:public"], ) + +filegroup( + name = "all_files", + testonly = True, + srcs = glob(["**"]), + visibility = ["//visibility:public"], +) diff --git a/go/runfiles/directory.go b/go/runfiles/directory.go index e5156ce46f..3a67e7eae2 100644 --- a/go/runfiles/directory.go +++ b/go/runfiles/directory.go @@ -21,8 +21,14 @@ import "path/filepath" // environmental variable RUNFILES_DIR. type Directory string -func (d Directory) new() *Runfiles { - return &Runfiles{d, directoryVar + "=" + string(d)} +func (d Directory) new(sourceRepo SourceRepo) (*Runfiles, error) { + r := &Runfiles{ + impl: d, + env: directoryVar + "=" + string(d), + sourceRepo: string(sourceRepo), + } + err := r.loadRepoMapping() + return r, err } func (d Directory) path(s string) (string, error) { diff --git a/go/runfiles/global.go b/go/runfiles/global.go index a7fcdfd761..52ec356953 100644 --- a/go/runfiles/global.go +++ b/go/runfiles/global.go @@ -14,18 +14,26 @@ package runfiles -import "sync" +import ( + "regexp" + "runtime" + "sync" +) // Rlocation 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), Rlocation returns an error that wraps ErrEmpty. -func Rlocation(s string) (string, error) { +func Rlocation(path string) (string, error) { + return RlocationFrom(path, CallerRepository()) +} + +func RlocationFrom(path string, sourceRepo string) (string, error) { r, err := g.get() if err != nil { return "", err } - return r.Rlocation(s) + return r.WithSourceRepo(sourceRepo).Rlocation(path) } // Env returns additional environmental variables to pass to subprocesses. @@ -42,6 +50,35 @@ func Env() ([]string, error) { return r.Env(), nil } +var legacyExternalGeneratedFile = regexp.MustCompile(`^bazel-out[/][^/]+/bin/external/([^/]+)/`) +var legacyExternalFile = regexp.MustCompile(`^external/([^/]+)/`) + +// CurrentRepository returns the canonical name of the Bazel repository that +// contains the source file of the caller of CurrentRepository. +func CurrentRepository() string { + return callerRepository(1) +} + +// CallerRepository returns the canonical name of the Bazel repository that +// contains the source file of the caller of the function that itself calls +// CallerRepository. +func CallerRepository() string { + return callerRepository(2) +} + +func callerRepository(skip int) string { + _, file, _, _ := runtime.Caller(skip + 1) + if match := legacyExternalGeneratedFile.FindStringSubmatch(file); match != nil { + return match[1] + } + if match := legacyExternalFile.FindStringSubmatch(file); match != nil { + return match[1] + } + // If a file is not in an external repository, it is in the main repository, + // which has the empty string as its canonical name. + return "" +} + type global struct { once sync.Once runfiles *Runfiles diff --git a/go/runfiles/manifest.go b/go/runfiles/manifest.go index dac945c8b4..4d8e87987f 100644 --- a/go/runfiles/manifest.go +++ b/go/runfiles/manifest.go @@ -28,13 +28,18 @@ import ( // environmental variable RUNFILES_MANIFEST_FILE. type ManifestFile string -func (f ManifestFile) new() (*Runfiles, error) { +func (f ManifestFile) new(sourceRepo SourceRepo) (*Runfiles, error) { m, err := f.parse() if err != nil { return nil, err } - - return &Runfiles{m, manifestFileVar + "=" + string(f)}, nil + r := &Runfiles{ + impl: m, + env: manifestFileVar + "=" + string(f), + sourceRepo: string(sourceRepo), + } + err = r.loadRepoMapping() + return r, err } type manifest map[string]string diff --git a/go/runfiles/runfiles.go b/go/runfiles/runfiles.go index d45ec3b68f..bc57b17bd4 100644 --- a/go/runfiles/runfiles.go +++ b/go/runfiles/runfiles.go @@ -36,6 +36,7 @@ package runfiles import ( + "bufio" "errors" "fmt" "os" @@ -48,6 +49,11 @@ const ( manifestFileVar = "RUNFILES_MANIFEST_FILE" ) +type repoMappingKey struct { + sourceRepo string + targetRepoApparentName string +} + // Runfiles allows access to Bazel runfiles. Use New to create Runfiles // objects; the zero Runfiles object always returns errors. See // https://docs.bazel.build/skylark/rules.html#runfiles for some information on @@ -55,10 +61,14 @@ const ( type Runfiles struct { // We don’t need concurrency control since Runfiles objects are // immutable once created. - impl runfiles - env string + impl runfiles + env string + repoMapping map[repoMappingKey]string + sourceRepo string } +const noSourceRepoSentinel = "_not_a_valid_repository_name" + // New creates a given Runfiles object. By default, it uses os.Args and the // RUNFILES_MANIFEST_FILE and RUNFILES_DIR environmental variables to find the // runfiles location. This can be overwritten by passing some options. @@ -67,22 +77,27 @@ type Runfiles struct { // https://docs.google.com/document/d/e/2PACX-1vSDIrFnFvEYhKsCMdGdD40wZRBX3m3aZ5HhVj4CtHPmiXKDCxioTUbYsDydjKtFDAzER5eg7OjJWs3V/pub. func New(opts ...Option) (*Runfiles, error) { var o options + o.sourceRepo = noSourceRepoSentinel for _, a := range opts { a.apply(&o) } + if o.sourceRepo == noSourceRepoSentinel { + o.sourceRepo = SourceRepo(CallerRepository()) + } + if o.manifest == "" { o.manifest = ManifestFile(os.Getenv(manifestFileVar)) } if o.manifest != "" { - return o.manifest.new() + return o.manifest.new(o.sourceRepo) } if o.directory == "" { o.directory = Directory(os.Getenv(directoryVar)) } if o.directory != "" { - return o.directory.new(), nil + return o.directory.new(o.sourceRepo) } if o.program == "" { @@ -90,12 +105,12 @@ func New(opts ...Option) (*Runfiles, error) { } manifest := ManifestFile(o.program + ".runfiles_manifest") if stat, err := os.Stat(string(manifest)); err == nil && stat.Mode().IsRegular() { - return manifest.new() + return manifest.new(o.sourceRepo) } dir := Directory(o.program + ".runfiles") if stat, err := os.Stat(string(dir)); err == nil && stat.IsDir() { - return dir.new(), nil + return dir.new(o.sourceRepo) } return nil, errors.New("runfiles: no runfiles found") @@ -132,7 +147,16 @@ func (r *Runfiles) Rlocation(path string) (string, error) { return path, nil } - p, err := r.impl.path(path) + mappedPath := path + split := strings.SplitN(path, "/", 2) + if len(split) == 2 { + key := repoMappingKey{r.sourceRepo, split[0]} + if targetRepoDirectory, exists := r.repoMapping[key]; exists { + mappedPath = targetRepoDirectory + "/" + split[1] + } + } + + p, err := r.impl.path(mappedPath) if err != nil { return "", Error{path, err} } @@ -152,6 +176,20 @@ func isNormalizedPath(s string) error { return nil } +// loadRepoMapping loads the repo mapping (if it exists) using the impl. +// This mutates the Runfiles object, but is idempotent. +func (r *Runfiles) loadRepoMapping() error { + repoMappingPath, err := r.impl.path(repoMappingRlocation) + // If Bzlmod is disabled, the repository mapping manifest isn't created, so + // it is not an error if it is missing. + if err != nil { + return nil + } + r.repoMapping, err = parseRepoMapping(repoMappingPath) + // If the repository mapping manifest exists, it must be valid. + return err +} + // 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 @@ -166,15 +204,33 @@ func (r *Runfiles) Env() []string { return []string{r.env} } +// WithSourceRepo returns a Runfiles instance identical to the current one, +// except that it uses the given repository's repository mapping when resolving +// runfiles paths. +func (r *Runfiles) WithSourceRepo(sourceRepo string) *Runfiles { + if r.sourceRepo == sourceRepo { + return r + } + clone := *r + clone.sourceRepo = sourceRepo + return &clone +} + // Option is an option for the New function to override runfiles discovery. type Option interface { apply(*options) } -// ProgramName is an Option that sets the program name. If not set, New uses +// ProgramName is an Option that sets the program name. If not set, New uses // os.Args[0]. type ProgramName string +// SourceRepo is an Option that sets the canonical name of the repository whose +// repository mapping should be used to resolve runfiles paths. If not set, New +// uses the repository containing the source file from which New is called. +// Use CurrentRepository to get the name of the current repository. +type SourceRepo string + // Error represents a failure to look up a runfile. type Error struct { // Runfile name that caused the failure. @@ -197,15 +253,53 @@ func (e Error) Unwrap() error { return e.Err } var ErrEmpty = errors.New("empty runfile") type options struct { - program ProgramName - manifest ManifestFile - directory Directory + program ProgramName + manifest ManifestFile + directory Directory + sourceRepo SourceRepo } func (p ProgramName) apply(o *options) { o.program = p } func (m ManifestFile) apply(o *options) { o.manifest = m } func (d Directory) apply(o *options) { o.directory = d } +func (sr SourceRepo) apply(o *options) { o.sourceRepo = sr } type runfiles interface { path(string) (string, error) } + +// The runfiles root symlink under which the repository mapping can be found. +// https://cs.opensource.google/bazel/bazel/+/1b073ac0a719a09c9b2d1a52680517ab22dc971e:src/main/java/com/google/devtools/build/lib/analysis/Runfiles.java;l=424 +const repoMappingRlocation = "_repo_mapping" + +// Parses a repository mapping manifest file emitted with Bzlmod enabled. +func parseRepoMapping(path string) (map[repoMappingKey]string, error) { + r, err := os.Open(path) + if err != nil { + // The repo mapping manifest only exists with Bzlmod, so it's not an + // error if it's missing. Since any repository name not contained in the + // mapping is assumed to be already canonical, an empty map is + // equivalent to not applying any mapping. + return nil, nil + } + defer r.Close() + + // Each line of the repository mapping manifest has the form: + // canonical name of source repo,apparent name of target repo,target repo runfiles directory + // https://cs.opensource.google/bazel/bazel/+/1b073ac0a719a09c9b2d1a52680517ab22dc971e:src/main/java/com/google/devtools/build/lib/analysis/RepoMappingManifestAction.java;l=117 + s := bufio.NewScanner(r) + repoMapping := make(map[repoMappingKey]string) + for s.Scan() { + fields := strings.SplitN(s.Text(), ",", 3) + if len(fields) != 3 { + return nil, fmt.Errorf("runfiles: bad repo mapping line %q in file %s", s.Text(), path) + } + repoMapping[repoMappingKey{fields[0], fields[1]}] = fields[2] + } + + if err = s.Err(); err != nil { + return nil, fmt.Errorf("runfiles: error parsing repo mapping file %s: %w", path, err) + } + + return repoMapping, nil +} diff --git a/tests/bcr/.bazelversion b/tests/bcr/.bazelversion index 5e52a62d72..50a4d4ef63 100644 --- a/tests/bcr/.bazelversion +++ b/tests/bcr/.bazelversion @@ -1 +1 @@ -last_green +839ce7f5c40240d8b6f49c416c3769e226f43fee diff --git a/tests/bcr/MODULE.bazel b/tests/bcr/MODULE.bazel index 8b96dce787..e796d8d003 100644 --- a/tests/bcr/MODULE.bazel +++ b/tests/bcr/MODULE.bazel @@ -10,6 +10,11 @@ local_path_override( ) bazel_dep(name = "gazelle", version = "0.26.0") bazel_dep(name = "protobuf", version = "3.19.6") +bazel_dep(name = "other_module", version = "") +local_path_override( + module_name = "other_module", + path = "other_module", +) # Test that this correctly downloads the SDK by requesting it from the commandline (see presubmit.yml). go_sdk = use_extension("@my_rules_go//go:extensions.bzl", "go_sdk") diff --git a/tests/bcr/other_module/BUILD.bazel b/tests/bcr/other_module/BUILD.bazel new file mode 100644 index 0000000000..657817096a --- /dev/null +++ b/tests/bcr/other_module/BUILD.bazel @@ -0,0 +1 @@ +exports_files(["bar.txt"]) diff --git a/tests/bcr/other_module/MODULE.bazel b/tests/bcr/other_module/MODULE.bazel new file mode 100644 index 0000000000..f851b8d08b --- /dev/null +++ b/tests/bcr/other_module/MODULE.bazel @@ -0,0 +1,3 @@ +module(name = "other_module") + +bazel_dep(name = "rules_go", version = "") diff --git a/tests/bcr/other_module/WORKSPACE b/tests/bcr/other_module/WORKSPACE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/bcr/other_module/bar.txt b/tests/bcr/other_module/bar.txt new file mode 100644 index 0000000000..ce01362503 --- /dev/null +++ b/tests/bcr/other_module/bar.txt @@ -0,0 +1 @@ +hello diff --git a/tests/bcr/runfiles/BUILD.bazel b/tests/bcr/runfiles/BUILD.bazel new file mode 100644 index 0000000000..d86ff26e3c --- /dev/null +++ b/tests/bcr/runfiles/BUILD.bazel @@ -0,0 +1,9 @@ +load("@my_rules_go//go:def.bzl", "go_test") + +go_test( + name = "runfiles_test", + srcs = ["runfiles_test.go"], + args = ["$(rlocationpath @other_module//:bar.txt)"], + data = ["@other_module//:bar.txt"], + deps = ["@my_rules_go//go/runfiles"], +) diff --git a/tests/bcr/runfiles/runfiles_test.go b/tests/bcr/runfiles/runfiles_test.go new file mode 100644 index 0000000000..ea82e97638 --- /dev/null +++ b/tests/bcr/runfiles/runfiles_test.go @@ -0,0 +1,67 @@ +package runfiles + +import ( + "os" + "testing" + + "github.com/bazelbuild/rules_go/go/runfiles" +) + +func TestRunfilesApparent(t *testing.T) { + path, err := runfiles.Rlocation("other_module/bar.txt") + if err != nil { + t.Fatalf("runfiles.Path: %v", err) + } + assertRunfile(t, path) +} + +func TestRunfilesApparentSourceRepositoryOption(t *testing.T) { + r, err := runfiles.New(runfiles.SourceRepo(runfiles.CurrentRepository())) + if err != nil { + t.Fatalf("runfiles.New: %v", err) + } + path, err := r.Rlocation("other_module/bar.txt") + if err != nil { + t.Fatalf("runfiles.Path: %v", err) + } + assertRunfile(t, path) +} + +func TestRunfilesApparentWithSourceRepository(t *testing.T) { + r, err := runfiles.New() + if err != nil { + t.Fatalf("runfiles.New: %v", err) + } + r = r.WithSourceRepo(runfiles.CurrentRepository()) + path, err := r.Rlocation("other_module/bar.txt") + if err != nil { + t.Fatalf("runfiles.Path: %v", err) + } + assertRunfile(t, path) +} + +func TestRunfilesFromApparent(t *testing.T) { + path, err := runfiles.RlocationFrom("other_module/bar.txt", runfiles.CurrentRepository()) + if err != nil { + t.Fatalf("runfiles.Path: %v", err) + } + assertRunfile(t, path) +} + +func TestRunfilesCanonical(t *testing.T) { + path, err := runfiles.Rlocation(os.Args[1]) + if err != nil { + t.Fatalf("runfiles.Path: %v", err) + } + assertRunfile(t, path) +} + +func assertRunfile(t *testing.T, path string) { + content, err := os.ReadFile(path) + if err != nil { + t.Fatalf("os.ReadFile: %v", err) + } + if string(content) != "hello\n" { + t.Fatalf("got %q; want %q", content, "hello\n") + } +} diff --git a/tests/runfiles/BUILD.bazel b/tests/runfiles/BUILD.bazel index 99334bafef..b657d307ef 100644 --- a/tests/runfiles/BUILD.bazel +++ b/tests/runfiles/BUILD.bazel @@ -13,6 +13,7 @@ # limitations under the License. load("@io_bazel_rules_go//go:def.bzl", "go_test") +load("@io_bazel_rules_go//go/tools/bazel_testing:def.bzl", "go_bazel_test") go_test( name = "runfiles_test", @@ -30,6 +31,11 @@ go_test( ], ) +go_bazel_test( + name = "runfiles_bazel_test", + srcs = ["runfiles_bazel_test.go"], +) + exports_files( ["test.txt"], visibility = ["//tests/runfiles/testprog:__pkg__"], diff --git a/tests/runfiles/runfiles_bazel_test.go b/tests/runfiles/runfiles_bazel_test.go new file mode 100644 index 0000000000..f73f17e42a --- /dev/null +++ b/tests/runfiles/runfiles_bazel_test.go @@ -0,0 +1,168 @@ +// Copyright 2019 The Bazel Authors. All rights reserved. +// +// 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 +// +// http://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_test + +import ( + "regexp" + "testing" + + "github.com/bazelbuild/rules_go/go/tools/bazel_testing" +) + +func TestMain(m *testing.M) { + bazel_testing.TestMain(m, bazel_testing.Args{ + Main: ` +-- other_repo/WORKSPACE -- +-- other_repo/pkg/BUILD.bazel -- +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "external_source_lib", + srcs = ["external_source_lib.go"], + importpath = "example.com/runfiles/external_source_lib", + deps = [ + "@io_bazel_rules_go//go/runfiles", + ], + visibility = ["//visibility:public"], +) + +genrule( + name = "gen_source", + srcs = ["external_source_lib.go"], + outs = ["external_generated_lib.go"], + cmd = "cat $(location external_source_lib.go) | sed 's/external_source_lib/external_generated_lib/g' > $@", +) + +go_library( + name = "external_generated_lib", + srcs = [":gen_source"], + importpath = "example.com/runfiles/external_generated_lib", + deps = [ + "@io_bazel_rules_go//go/runfiles", + ], + visibility = ["//visibility:public"], +) +-- other_repo/pkg/external_source_lib.go -- +package external_source_lib + +import ( + "fmt" + "runtime" + + "github.com/bazelbuild/rules_go/go/runfiles" +) + +func PrintRepo() { + _, file, _, _ := runtime.Caller(0) + fmt.Printf("%s: '%s'\n", file, runfiles.CurrentRepository()) +} +-- pkg/BUILD.bazel -- +-- pkg/BUILD.bazel -- +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "internal_source_lib", + srcs = ["internal_source_lib.go"], + importpath = "example.com/runfiles/internal_source_lib", + deps = [ + "@io_bazel_rules_go//go/runfiles", + ], + visibility = ["//visibility:public"], +) + +genrule( + name = "gen_source", + srcs = ["internal_source_lib.go"], + outs = ["internal_generated_lib.go"], + cmd = "cat $(location internal_source_lib.go) | sed 's/internal_source_lib/internal_generated_lib/g' > $@", +) + +go_library( + name = "internal_generated_lib", + srcs = [":gen_source"], + importpath = "example.com/runfiles/internal_generated_lib", + deps = [ + "@io_bazel_rules_go//go/runfiles", + ], + visibility = ["//visibility:public"], +) +-- pkg/internal_source_lib.go -- +package internal_source_lib + +import ( + "fmt" + "runtime" + + "github.com/bazelbuild/rules_go/go/runfiles" +) + +func PrintRepo() { + _, file, _, _ := runtime.Caller(0) + fmt.Printf("%s: '%s'\n", file, runfiles.CurrentRepository()) +} +-- BUILD.bazel -- +load("@io_bazel_rules_go//go:def.bzl", "go_binary") + +go_binary( + name = "main", + srcs = ["main.go"], + deps = [ + "//pkg:internal_source_lib", + "//pkg:internal_generated_lib", + "@other_repo//pkg:external_source_lib", + "@other_repo//pkg:external_generated_lib", + ], +) +-- main.go -- +package main + +import ( + "example.com/runfiles/internal_generated_lib" + "example.com/runfiles/internal_source_lib" + "example.com/runfiles/external_generated_lib" + "example.com/runfiles/external_source_lib" +) + +func main() { + internal_source_lib.PrintRepo() + internal_generated_lib.PrintRepo() + external_source_lib.PrintRepo() + external_generated_lib.PrintRepo() +} +`, + WorkspaceSuffix: ` +local_repository( + name = "other_repo", + path = "other_repo", +) +`, + }) +} + +var expectedOutputLegacy = regexp.MustCompile(`^pkg/internal_source_lib.go: '' +bazel-out/[^/]+/bin/pkg/internal_generated_lib.go: '' +external/other_repo/pkg/external_source_lib.go: 'other_repo' +bazel-out/[^/]+/bin/external/other_repo/pkg/external_generated_lib.go: 'other_repo' +$`) + +func TestCurrentRepository(t *testing.T) { + out, err := bazel_testing.BazelOutput("run", "//:main") + if err != nil { + t.Fatal(err) + } + if !expectedOutputLegacy.Match(out) { + t.Fatalf("got: %q, want: %q", string(out), expectedOutputLegacy.String()) + } +}