blob: 343b7ff8129ae06576138c97275e1ee19812b93c [file] [log] [blame]
Serge Bazanskib76580c2021-03-31 22:07:01 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17// gotoolwrap is a tiny wrapper used to run executables that expect a standard
18// Go development environment setup to act on the monogon workspace. Notably,
19// it's used in Bazel rules to run tools like gofmt when invoked by some kinds
20// of code generation tools.
21//
22// Usage: ./gotoolwrap executable arg1 arg2
23//
24// gotoolwrap expects the following environment variables to be set (and unsets
25// them before calling the given executable):
26// - GOTOOLWRAP_GOPATH: A synthetic GOPATH, eg. one generated by rules_go's
27// go_path target.
28// - GOTOOLWRAP_GOROOT: A Go SDK's GOROOT, eg. one from rules_go's GoSDK
29// provider.
Tim Windelschmidtddc5e6a2024-04-23 23:44:34 +020030// - GOTOOLWRAP_COPYOUT: Pairs of source:destination elements separated by ;.
31// Files given in this list will be copied from source
32// (relative to the GOPATH sources) to the destination
33// (absolute path). This is primarily useful when dealing
34// with code generators which insist on putting their
35// generated files within the GOPATH, and allows copying
36// of said files to a declared output file.
Serge Bazanskib76580c2021-03-31 22:07:01 +020037//
38// gotoolwrap will set PATH to contain GOROOT/bin, and set GOPATH and GOROOT as
39// resolved, absolute paths. Absolute paths are expected by tools like 'gofmt'.
40
41package main
42
43import (
44 "errors"
45 "fmt"
Serge Bazanskib76580c2021-03-31 22:07:01 +020046 "log"
47 "os"
48 "os/exec"
49 "path/filepath"
50 "strings"
51)
52
53func main() {
54 gopath := os.Getenv("GOTOOLWRAP_GOPATH")
55 if gopath == "" {
56 log.Fatal("GOTOOLWRAP_GOPATH must be set")
57 }
58
59 goroot := os.Getenv("GOTOOLWRAP_GOROOT")
60 if goroot == "" {
61 log.Fatal("GOTOOLWRAP_GOROOT must be set")
62 }
63
64 if len(os.Args) < 2 {
65 log.Fatalf("No command specified")
66 }
67
68 // Resolve gopath and goroot to absolute paths.
69 gopathAbs, err := filepath.Abs(gopath)
70 if err != nil {
71 log.Fatalf("Abs(%q): %v", gopath, err)
72 }
73 gorootAbs, err := filepath.Abs(goroot)
74 if err != nil {
75 log.Fatalf("Abs(%q): %v", goroot, err)
76 }
77
78 // Ensure the resolved GOROOT has a bin/go and bin/gofmt.
79 gorootBin := filepath.Join(gorootAbs, "bin")
80 stat, err := os.Stat(gorootBin)
81 if err != nil {
82 log.Fatalf("Could not stat $GOTOOLWRAP_GOROOT/bin (%q): %v", gorootBin, err)
83 }
84 if !stat.IsDir() {
85 log.Fatalf("$GOTOOLWRAP_GOROOT/bin (%q) is not a directory", gorootBin)
86 }
87 // We list all files inside so that we can print them to the user for
88 // debugging purposes if that's not the case.
89 binFiles := make(map[string]bool)
Lorenz Brun764a2de2021-11-22 16:26:36 +010090 files, err := os.ReadDir(gorootBin)
Serge Bazanskib76580c2021-03-31 22:07:01 +020091 if err != nil {
92 log.Fatalf("Could not read dir $GOTOOLWRAP_GOROOT/bin (%q): %v", gorootBin, err)
93 }
94 for _, f := range files {
95 if f.IsDir() {
96 continue
97 }
98 binFiles[f.Name()] = true
99 }
100 if !binFiles["go"] || !binFiles["gofmt"] {
101 msg := "no files"
102 if len(binFiles) > 0 {
103 var names []string
Tim Windelschmidt6b6428d2024-04-11 01:35:41 +0200104 for name := range binFiles {
Serge Bazanskib76580c2021-03-31 22:07:01 +0200105 names = append(names, fmt.Sprintf("%q", name))
106 }
107 msg = fmt.Sprintf(": %s", strings.Join(names, ", "))
108 }
109 log.Fatalf("$GOTOOLWRAP_GOROOT/bin (%q) does not contain go and/or gofmt, found %s", gorootBin, msg)
110 }
111
112 // Make new PATH.
113 path := os.Getenv("PATH")
114 if path == "" {
115 path = gorootBin
116 } else {
117 path = fmt.Sprintf("%s:%s", gorootBin, path)
118 }
119
120 cmd := exec.Command(os.Args[1], os.Args[2:]...)
121
122 // Copy current env into command's env, filtering out GOTOOLWRAP env vars
123 // and PATH (which we set ourselves).
124 for _, v := range os.Environ() {
125 if strings.HasPrefix(v, "GOTOOLWRAP_GOROOT=") {
126 continue
127 }
128 if strings.HasPrefix(v, "GOTOOLWRAP_GOPATH=") {
129 continue
130 }
Tim Windelschmidtddc5e6a2024-04-23 23:44:34 +0200131 if strings.HasPrefix(v, "GOTOOLWRAP_COPYOUT=") {
132 continue
133 }
Serge Bazanskib76580c2021-03-31 22:07:01 +0200134 if strings.HasPrefix(v, "PATH=") {
135 continue
136 }
137 cmd.Env = append(cmd.Env, v)
138 }
Lorenz Brunc2e3b1b2021-11-11 11:06:41 +0100139 // Create a temporary cache directory and remove everything afterwards.
140 // Based on code from
141 // @io_bazel_rules_go//go/tools/builders:stdliblist.go L174
142 tempDir, err := os.MkdirTemp("", "gocache")
143 if err != nil {
144 log.Fatalf("Cannot create temporary directory: %v", err)
145 }
146 defer os.RemoveAll(tempDir)
147
Serge Bazanskib76580c2021-03-31 22:07:01 +0200148 cmd.Env = append(cmd.Env,
Tim Windelschmidtddc5e6a2024-04-23 23:44:34 +0200149 "GOROOT="+gorootAbs,
150 "GOPATH="+gopathAbs,
151 "PATH="+path,
Tim Windelschmidt92316fd2024-04-18 23:06:40 +0200152 "GO111MODULE=off",
Tim Windelschmidtddc5e6a2024-04-23 23:44:34 +0200153 "GOCACHE="+tempDir,
Serge Bazanskib76580c2021-03-31 22:07:01 +0200154 )
155
156 // Run the command interactively.
157 cmd.Stdout = os.Stdout
158 cmd.Stderr = os.Stderr
159 cmd.Stdin = os.Stdin
160 if err := cmd.Run(); err != nil {
161 var exitErr *exec.ExitError
162 if errors.As(err, &exitErr) {
163 os.Exit(exitErr.ExitCode())
164 } else {
165 log.Fatalf("Could not run %q: %v", os.Args[1], err)
166 }
167 }
Tim Windelschmidtddc5e6a2024-04-23 23:44:34 +0200168
169 copyout := os.Getenv("GOTOOLWRAP_COPYOUT")
170 if len(copyout) != 0 {
171 for _, pair := range strings.Split(copyout, ";") {
172 parts := strings.Split(pair, ":")
173 if len(parts) != 2 {
174 log.Fatalf("GOTOOL_COPYOUT invalid pair: %q", pair)
175 }
176 from := filepath.Join(gopathAbs, "src", parts[0])
177 to := parts[1]
178 log.Printf("gotoolwrap: Copying %s to %s...", from, to)
179 data, err := os.ReadFile(from)
180 if err != nil {
181 log.Fatalf("gotoolwrap: read failed: %v", err)
182 }
183 err = os.MkdirAll(filepath.Dir(to), 0755)
184 if err != nil {
185 log.Fatalf("gotoolwrap: mkdir failed: %v", err)
186 }
187 err = os.WriteFile(to, data, 0644)
188 if err != nil {
189 log.Fatalf("gotoolwrap: write failed: %v", err)
190 }
191 }
192 }
Serge Bazanskib76580c2021-03-31 22:07:01 +0200193}