github.com/gopherjs/gopherjs@v1.19.0-beta1.0.20240506212314-27071a8796e4/build/context.go (about) 1 package build 2 3 import ( 4 "fmt" 5 "go/build" 6 "go/token" 7 "io" 8 "net/http" 9 "os" 10 "os/exec" 11 "path" 12 "path/filepath" 13 "sort" 14 "strings" 15 16 _ "github.com/gopherjs/gopherjs/build/versionhack" // go/build release tags hack. 17 "github.com/gopherjs/gopherjs/compiler" 18 "github.com/gopherjs/gopherjs/compiler/gopherjspkg" 19 "github.com/gopherjs/gopherjs/compiler/natives" 20 "golang.org/x/tools/go/buildutil" 21 ) 22 23 // Env contains build environment configuration required to define an instance 24 // of XContext. 25 type Env struct { 26 GOROOT string 27 GOPATH string 28 29 GOOS string 30 GOARCH string 31 32 BuildTags []string 33 InstallSuffix string 34 } 35 36 // DefaultEnv creates a new instance of build Env according to environment 37 // variables. 38 // 39 // By default, GopherJS will use GOOS=js GOARCH=ecmascript to build non-standard 40 // library packages. If GOOS or GOARCH environment variables are set and not 41 // empty, user-provided values will be used instead. This is done to facilitate 42 // transition from the legacy GopherJS behavior, which used native GOOS, and may 43 // be removed in future. 44 func DefaultEnv() Env { 45 e := Env{} 46 e.GOROOT = DefaultGOROOT 47 e.GOPATH = build.Default.GOPATH 48 49 if val := os.Getenv("GOOS"); val != "" { 50 e.GOOS = val 51 } else { 52 e.GOOS = "js" 53 } 54 55 if val := os.Getenv("GOARCH"); val != "" { 56 e.GOARCH = val 57 } else { 58 e.GOARCH = "ecmascript" 59 } 60 return e 61 } 62 63 // XContext is an extension of go/build.Context with GopherJS-specific features. 64 // 65 // It abstracts away several different sources GopherJS can load its packages 66 // from, with a minimal API. 67 type XContext interface { 68 // Import returns details about the Go package named by the importPath, 69 // interpreting local import paths relative to the srcDir directory. 70 Import(path string, srcDir string, mode build.ImportMode) (*PackageData, error) 71 72 // Env returns build environment configuration this context has been set up for. 73 Env() Env 74 75 // Match explans build patterns into a set of matching import paths (see go help packages). 76 Match(patterns []string) ([]string, error) 77 } 78 79 // simpleCtx is a wrapper around go/build.Context with support for GopherJS-specific 80 // features. 81 type simpleCtx struct { 82 bctx build.Context 83 isVirtual bool // Imported packages don't have a physical directory on disk. 84 noPostTweaks bool // Don't apply post-load tweaks to packages. For tests only. 85 } 86 87 // Import implements XContext.Import(). 88 func (sc simpleCtx) Import(importPath string, srcDir string, mode build.ImportMode) (*PackageData, error) { 89 bctx, mode := sc.applyPreloadTweaks(importPath, srcDir, mode) 90 pkg, err := bctx.Import(importPath, srcDir, mode) 91 if err != nil { 92 return nil, err 93 } 94 jsFiles, err := jsFilesFromDir(&sc.bctx, pkg.Dir) 95 if err != nil { 96 return nil, fmt.Errorf("failed to enumerate .inc.js files in %s: %w", pkg.Dir, err) 97 } 98 if !path.IsAbs(pkg.Dir) { 99 pkg.Dir = mustAbs(pkg.Dir) 100 } 101 pkg = sc.applyPostloadTweaks(pkg) 102 103 return &PackageData{ 104 Package: pkg, 105 IsVirtual: sc.isVirtual, 106 JSFiles: jsFiles, 107 bctx: &sc.bctx, 108 }, nil 109 } 110 111 // Match implements XContext.Match. 112 func (sc simpleCtx) Match(patterns []string) ([]string, error) { 113 if sc.isVirtual { 114 // We can't use go tool to enumerate packages in a virtual file system, 115 // so we fall back onto a simpler implementation provided by the buildutil 116 // package. It doesn't support all valid patterns, but should be good enough. 117 // 118 // Note: this code path will become unnecessary after 119 // https://github.com/gopherjs/gopherjs/issues/1021 is implemented. 120 args := []string{} 121 for _, p := range patterns { 122 switch p { 123 case "all": 124 args = append(args, "...") 125 case "std", "main", "cmd": 126 // These patterns are not supported by buildutil.ExpandPatterns(), 127 // but they would be matched by the real context correctly, so skip them. 128 default: 129 args = append(args, p) 130 } 131 } 132 matches := []string{} 133 for importPath := range buildutil.ExpandPatterns(&sc.bctx, args) { 134 if importPath[0] == '.' { 135 p, err := sc.Import(importPath, ".", build.FindOnly) 136 // Resolve relative patterns into canonical import paths. 137 if err != nil { 138 continue 139 } 140 importPath = p.ImportPath 141 } 142 matches = append(matches, importPath) 143 } 144 sort.Strings(matches) 145 return matches, nil 146 } 147 148 args := append([]string{ 149 "-e", "-compiler=gc", 150 "-tags=" + strings.Join(sc.bctx.BuildTags, ","), 151 "-installsuffix=" + sc.bctx.InstallSuffix, 152 "-f={{.ImportPath}}", 153 "--", 154 }, patterns...) 155 156 out, err := sc.gotool("list", args...) 157 if err != nil { 158 return nil, fmt.Errorf("failed to list packages on FS: %w", err) 159 } 160 matches := strings.Split(strings.TrimSpace(out), "\n") 161 sort.Strings(matches) 162 return matches, nil 163 } 164 165 func (sc simpleCtx) Env() Env { 166 return Env{ 167 GOROOT: sc.bctx.GOROOT, 168 GOPATH: sc.bctx.GOPATH, 169 GOOS: sc.bctx.GOOS, 170 GOARCH: sc.bctx.GOARCH, 171 BuildTags: sc.bctx.BuildTags, 172 InstallSuffix: sc.bctx.InstallSuffix, 173 } 174 } 175 176 // gotool executes the go tool set up for the build context and returns standard output. 177 func (sc simpleCtx) gotool(subcommand string, args ...string) (string, error) { 178 if sc.isVirtual { 179 panic(fmt.Errorf("can't use go tool with a virtual build context")) 180 } 181 args = append([]string{subcommand}, args...) 182 cmd := exec.Command(filepath.Join(sc.bctx.GOROOT, "bin", "go"), args...) 183 184 if sc.bctx.Dir != "" { 185 cmd.Dir = sc.bctx.Dir 186 } 187 188 var stdout, stderr strings.Builder 189 cmd.Stdout = &stdout 190 cmd.Stderr = &stderr 191 192 cgo := "0" 193 if sc.bctx.CgoEnabled { 194 cgo = "1" 195 } 196 cmd.Env = append(os.Environ(), 197 "GOOS="+sc.bctx.GOOS, 198 "GOARCH="+sc.bctx.GOARCH, 199 "GOROOT="+sc.bctx.GOROOT, 200 "GOPATH="+sc.bctx.GOPATH, 201 "CGO_ENABLED="+cgo, 202 ) 203 204 if err := cmd.Run(); err != nil { 205 return "", fmt.Errorf("go tool error: %v: %w\n%s", cmd, err, stderr.String()) 206 } 207 return stdout.String(), nil 208 } 209 210 // applyPreloadTweaks makes several package-specific adjustments to package importing. 211 // 212 // Ideally this method would not be necessary, but currently several packages 213 // require special handing in order to be compatible with GopherJS. This method 214 // returns a copy of the build context, keeping the original one intact. 215 func (sc simpleCtx) applyPreloadTweaks(importPath string, srcDir string, mode build.ImportMode) (build.Context, build.ImportMode) { 216 bctx := sc.bctx 217 if sc.isStd(importPath, srcDir) { 218 // For most of the platform-dependent code in the standard library we simply 219 // reuse implementations targeting WebAssembly. For the user-supplied we use 220 // regular gopherjs-specific GOOS/GOARCH. 221 bctx.GOOS = "js" 222 bctx.GOARCH = "wasm" 223 } 224 switch importPath { 225 case "github.com/gopherjs/gopherjs/js", "github.com/gopherjs/gopherjs/nosync": 226 // These packages are already embedded via gopherjspkg.FS virtual filesystem 227 // (which can be safely vendored). Don't try to use vendor directory to 228 // resolve them. 229 mode |= build.IgnoreVendor 230 } 231 232 return bctx, mode 233 } 234 235 // applyPostloadTweaks makes adjustments to the contents of the loaded package. 236 // 237 // Some of the standard library packages require additional tweaks that are not 238 // covered by our augmentation logic, for example excluding or including 239 // particular source files. This method ensures that all such tweaks are applied 240 // before the package is returned to the caller. 241 func (sc simpleCtx) applyPostloadTweaks(pkg *build.Package) *build.Package { 242 if sc.isVirtual { 243 // GopherJS overlay package sources don't need tweaks to their content, 244 // since we already control them directly. 245 return pkg 246 } 247 if sc.noPostTweaks { 248 return pkg 249 } 250 switch pkg.ImportPath { 251 case "runtime": 252 pkg.GoFiles = []string{} // Package sources are completely replaced in natives. 253 case "runtime/pprof": 254 pkg.GoFiles = nil 255 case "sync": 256 // GopherJS completely replaces sync.Pool implementation with a simpler one, 257 // since it always executes in a single-threaded environment. 258 pkg.GoFiles = exclude(pkg.GoFiles, "pool.go") 259 case "syscall/js": 260 // Reuse upstream tests to ensure conformance, but completely replace 261 // implementation. 262 pkg.GoFiles = []string{} 263 pkg.TestGoFiles = []string{} 264 } 265 266 pkg.Imports, pkg.ImportPos = updateImports(pkg.GoFiles, pkg.ImportPos) 267 pkg.TestImports, pkg.TestImportPos = updateImports(pkg.TestGoFiles, pkg.TestImportPos) 268 pkg.XTestImports, pkg.XTestImportPos = updateImports(pkg.XTestGoFiles, pkg.XTestImportPos) 269 270 return pkg 271 } 272 273 // isStd returns true if the given importPath resolves into a standard library 274 // package. Relative paths are interpreted relative to srcDir. 275 func (sc simpleCtx) isStd(importPath, srcDir string) bool { 276 pkg, err := sc.bctx.Import(importPath, srcDir, build.FindOnly) 277 if err != nil { 278 return false 279 } 280 return pkg.Goroot 281 } 282 283 var defaultBuildTags = []string{ 284 "netgo", // See https://godoc.org/net#hdr-Name_Resolution. 285 "purego", // See https://golang.org/issues/23172. 286 "math_big_pure_go", // Use pure Go version of math/big. 287 // We can't set compiler to gopherjs, since Go tooling doesn't support that, 288 // but, we can at least always set this build tag. 289 "gopherjs", 290 } 291 292 // embeddedCtx creates simpleCtx that imports from a virtual FS embedded into 293 // the GopherJS compiler. 294 func embeddedCtx(embedded http.FileSystem, e Env) *simpleCtx { 295 fs := &vfs{embedded} 296 ec := goCtx(e) 297 ec.bctx.GOPATH = "" 298 299 // Path functions must behave unix-like to work with the VFS. 300 ec.bctx.JoinPath = path.Join 301 ec.bctx.SplitPathList = splitPathList 302 ec.bctx.IsAbsPath = path.IsAbs 303 ec.bctx.HasSubdir = hasSubdir 304 305 // Substitute real FS with the embedded one. 306 ec.bctx.IsDir = fs.IsDir 307 ec.bctx.ReadDir = fs.ReadDir 308 ec.bctx.OpenFile = fs.OpenFile 309 ec.isVirtual = true 310 return ec 311 } 312 313 // overlayCtx creates simpleCtx that imports from the embedded standard library 314 // overlays. 315 func overlayCtx(e Env) *simpleCtx { 316 return embeddedCtx(&withPrefix{fs: http.FS(natives.FS), prefix: e.GOROOT}, e) 317 } 318 319 // gopherjsCtx creates a simpleCtx that imports from the embedded gopherjs 320 // packages in case they are not present in the user's source tree. 321 func gopherjsCtx(e Env) *simpleCtx { 322 gopherjsRoot := filepath.Join(e.GOROOT, "src", "github.com", "gopherjs", "gopherjs") 323 return embeddedCtx(&withPrefix{gopherjspkg.FS, gopherjsRoot}, e) 324 } 325 326 // goCtx creates simpleCtx that imports from the real file system GOROOT, GOPATH 327 // or Go Modules. 328 func goCtx(e Env) *simpleCtx { 329 gc := simpleCtx{ 330 bctx: build.Context{ 331 GOROOT: e.GOROOT, 332 GOPATH: e.GOPATH, 333 GOOS: e.GOOS, 334 GOARCH: e.GOARCH, 335 InstallSuffix: e.InstallSuffix, 336 Compiler: "gc", 337 BuildTags: append(append([]string{}, e.BuildTags...), defaultBuildTags...), 338 CgoEnabled: false, // CGo is not supported by GopherJS. 339 340 // go/build supports modules, but only when no FS access functions are 341 // overridden and when provided ReleaseTags match those of the default 342 // context (matching Go compiler's version). 343 // This limitation stems from the fact that it will invoke the Go tool 344 // which can only see files on the real FS and will assume release tags 345 // based on the Go tool's version. 346 // 347 // See also comments to the versionhack package. 348 ReleaseTags: build.Default.ReleaseTags[:compiler.GoVersion], 349 }, 350 } 351 return &gc 352 } 353 354 // chainedCtx combines two build contexts. Secondary context acts as a fallback 355 // when a package is not found in the primary, and is ignored otherwise. 356 // 357 // This allows GopherJS to load its core "js" and "nosync" packages from the 358 // embedded VFS whenever user's code doesn't directly depend on them, but 359 // augmented stdlib does. 360 type chainedCtx struct { 361 primary XContext 362 secondary XContext 363 } 364 365 // Import implements buildCtx.Import(). 366 func (cc chainedCtx) Import(importPath string, srcDir string, mode build.ImportMode) (*PackageData, error) { 367 pkg, err := cc.primary.Import(importPath, srcDir, mode) 368 if err == nil { 369 return pkg, nil 370 } else if IsPkgNotFound(err) { 371 return cc.secondary.Import(importPath, srcDir, mode) 372 } else { 373 return nil, err 374 } 375 } 376 377 func (cc chainedCtx) Env() Env { return cc.primary.Env() } 378 379 // Match implements XContext.Match(). 380 // 381 // Packages from both contexts are included and returned as a deduplicated 382 // sorted list. 383 func (cc chainedCtx) Match(patterns []string) ([]string, error) { 384 m1, err := cc.primary.Match(patterns) 385 if err != nil { 386 return nil, fmt.Errorf("failed to list packages in the primary context: %s", err) 387 } 388 m2, err := cc.secondary.Match(patterns) 389 if err != nil { 390 return nil, fmt.Errorf("failed to list packages in the secondary context: %s", err) 391 } 392 393 seen := map[string]bool{} 394 matches := []string{} 395 for _, m := range append(m1, m2...) { 396 if seen[m] { 397 continue 398 } 399 seen[m] = true 400 matches = append(matches, m) 401 } 402 sort.Strings(matches) 403 return matches, nil 404 } 405 406 // IsPkgNotFound returns true if the error was caused by package not found. 407 // 408 // Unfortunately, go/build doesn't make use of typed errors, so we have to 409 // rely on the error message. 410 func IsPkgNotFound(err error) bool { 411 return err != nil && 412 (strings.Contains(err.Error(), "cannot find package") || // Modules off. 413 strings.Contains(err.Error(), "is not in GOROOT")) // Modules on. 414 } 415 416 // updateImports package's list of import paths to only those present in sources 417 // after post-load tweaks. 418 func updateImports(sources []string, importPos map[string][]token.Position) (newImports []string, newImportPos map[string][]token.Position) { 419 if importPos == nil { 420 // Short-circuit for tests when no imports are loaded. 421 return nil, nil 422 } 423 sourceSet := map[string]bool{} 424 for _, source := range sources { 425 sourceSet[source] = true 426 } 427 428 newImportPos = map[string][]token.Position{} 429 for importPath, positions := range importPos { 430 for _, pos := range positions { 431 if sourceSet[filepath.Base(pos.Filename)] { 432 newImportPos[importPath] = append(newImportPos[importPath], pos) 433 } 434 } 435 } 436 437 for importPath := range newImportPos { 438 newImports = append(newImports, importPath) 439 } 440 sort.Strings(newImports) 441 return newImports, newImportPos 442 } 443 444 // jsFilesFromDir finds and loads any *.inc.js packages in the build context 445 // directory. 446 func jsFilesFromDir(bctx *build.Context, dir string) ([]JSFile, error) { 447 files, err := buildutil.ReadDir(bctx, dir) 448 if err != nil { 449 return nil, err 450 } 451 var jsFiles []JSFile 452 for _, file := range files { 453 if !strings.HasSuffix(file.Name(), ".inc.js") || file.IsDir() { 454 continue 455 } 456 if file.Name()[0] == '_' || file.Name()[0] == '.' { 457 continue // Skip "hidden" files that are typically ignored by the Go build system. 458 } 459 460 path := buildutil.JoinPath(bctx, dir, file.Name()) 461 f, err := buildutil.OpenFile(bctx, path) 462 if err != nil { 463 return nil, fmt.Errorf("failed to open %s from %v: %w", path, bctx, err) 464 } 465 defer f.Close() 466 467 content, err := io.ReadAll(f) 468 if err != nil { 469 return nil, fmt.Errorf("failed to read %s from %v: %w", path, bctx, err) 470 } 471 472 jsFiles = append(jsFiles, JSFile{ 473 Path: path, 474 ModTime: file.ModTime(), 475 Content: content, 476 }) 477 } 478 return jsFiles, nil 479 }