github.com/please-build/go-rules/tools/please_go@v0.0.0-20240319165128-ea27d6f5caba/install/install.go (about) 1 package install 2 3 import ( 4 "bufio" 5 "encoding/json" 6 "fmt" 7 "go/build" 8 "log" 9 "os" 10 "path/filepath" 11 "strconv" 12 "strings" 13 14 "github.com/please-build/go-rules/tools/please_go/embed" 15 "github.com/please-build/go-rules/tools/please_go/install/exec" 16 "github.com/please-build/go-rules/tools/please_go/install/toolchain" 17 ) 18 19 const baseWorkDir = "_work" 20 const ldFlagsFile = "LD_FLAGS" 21 22 // PleaseGoInstall implements functionality similar to `go install` however it works with import configs to avoid a 23 // dependence on the GO_PATH, go.mod or other go build concepts. 24 type PleaseGoInstall struct { 25 buildContext build.Context 26 srcRoot string 27 moduleName string 28 importConfig string 29 outDir string 30 trimPath string 31 32 additionalCFlags string 33 // A set of flags we may get from: pkg-config, #cgo directives, 34 // go rules' `linker_flags` argument and `go.ldflags` config value. 35 collectedLdFlags []string 36 37 tc *toolchain.Toolchain 38 39 compiledPackages map[string]string 40 } 41 42 func (install *PleaseGoInstall) mustSetBuildContext(tags []string) { 43 install.buildContext = build.Default 44 install.buildContext.BuildTags = append(install.buildContext.BuildTags, tags...) 45 46 version, err := install.tc.GoMinorVersion() 47 if err != nil { 48 log.Fatalf("failed to determine go version: %v", err) 49 } 50 51 install.buildContext.ReleaseTags = []string{} 52 for i := 1; i <= version; i++ { 53 install.buildContext.ReleaseTags = append(install.buildContext.ReleaseTags, "go1."+strconv.Itoa(i)) 54 } 55 } 56 57 // New creates a new PleaseGoInstall 58 func New(buildTags []string, srcRoot, moduleName, importConfig, ldFlags, cFlags, goTool, ccTool, pkgConfTool, out, trimPath string) *PleaseGoInstall { 59 i := &PleaseGoInstall{ 60 srcRoot: srcRoot, 61 moduleName: moduleName, 62 importConfig: importConfig, 63 outDir: out, 64 trimPath: trimPath, 65 66 additionalCFlags: cFlags, 67 68 tc: &toolchain.Toolchain{ 69 CcTool: ccTool, 70 GoTool: goTool, 71 PkgConfigTool: pkgConfTool, 72 Exec: &exec.Executor{Stdout: os.Stdout, Stderr: os.Stderr}, 73 }, 74 } 75 if len(ldFlags) > 0 { 76 i.collectedLdFlags = []string{ldFlags} 77 } 78 i.mustSetBuildContext(buildTags) 79 return i 80 } 81 82 // Install will compile the provided packages. Packages can be wildcards i.e. `foo/...` which compiles all packages 83 // under the directory tree of `{module}/foo` 84 func (install *PleaseGoInstall) Install(packages []string) error { 85 if err := install.initBuildEnv(); err != nil { 86 return err 87 } 88 if err := install.parseImportConfig(); err != nil { 89 return err 90 } 91 92 for _, target := range packages { 93 if !strings.HasPrefix(target, install.moduleName) { 94 target = filepath.Join(install.moduleName, target) 95 } 96 if strings.HasSuffix(target, "/...") { 97 importRoot := strings.TrimSuffix(target, "/...") 98 err := install.compileAll(importRoot) 99 if err != nil { 100 return err 101 } 102 } else { 103 if err := install.compile([]string{}, target); err != nil { 104 return fmt.Errorf("failed to compile %v: %w", target, err) 105 } 106 107 pkg, err := install.importDir(target) 108 if err != nil { 109 panic(fmt.Sprintf("import dir failed after successful compilation: %v", err)) 110 } 111 if pkg.IsCommand() { 112 if err := install.linkPackage(target); err != nil { 113 return fmt.Errorf("failed to link %v: %w", target, err) 114 } 115 } 116 } 117 } 118 119 if err := install.writeLDFlags(); err != nil { 120 return fmt.Errorf("failed to write ld flags: %w", err) 121 } 122 123 return nil 124 } 125 126 func (install *PleaseGoInstall) writeLDFlags() error { 127 flagFile, err := os.Create(ldFlagsFile) 128 if err != nil { 129 return err 130 } 131 defer flagFile.Close() 132 133 _, err = flagFile.WriteString(strings.Join(install.collectedLdFlags, " ")) 134 return err 135 } 136 137 func (install *PleaseGoInstall) linkPackage(target string) error { 138 out := install.compiledPackages[target] 139 filename := strings.TrimSuffix(filepath.Base(out), ".a") 140 binName := filepath.Join(install.outDir, "bin", filename) 141 142 return install.tc.Link(out, binName, install.importConfig, install.collectedLdFlags) 143 } 144 145 // compileAll walks the provided directory looking for go packages to compile. Unlike compile(), this will skip any 146 // directories that contain no .go files for the current architecture. 147 func (install *PleaseGoInstall) compileAll(dir string) error { 148 pkgRoot := install.pkgDir(dir) 149 return filepath.WalkDir(pkgRoot, func(path string, info os.DirEntry, err error) error { 150 if err != nil { 151 return err 152 } 153 if !info.IsDir() { 154 relativePackage := filepath.Dir(strings.TrimPrefix(path, pkgRoot)) 155 if err := install.compile([]string{}, filepath.Join(dir, relativePackage)); err != nil { 156 switch err.(type) { 157 case *build.NoGoError: 158 // We might walk into a dir that has no .go files for the current arch. This shouldn't 159 // be an error so we just eat this 160 return nil 161 default: 162 return err 163 } 164 } 165 } else if info.Name() == "testdata" { 166 return filepath.SkipDir // Dirs named testdata are deemed not to contain buildable Go code. 167 } 168 return nil 169 }) 170 } 171 172 func (install *PleaseGoInstall) initBuildEnv() error { 173 if err := install.tc.Exec.Run("mkdir -p %s\n", filepath.Join(install.outDir, "bin")); err != nil { 174 return err 175 } 176 return install.tc.Exec.Run("touch %s", ldFlagsFile) 177 } 178 179 // pkgDir returns the file path to the given target package 180 func (install *PleaseGoInstall) pkgDir(target string) string { 181 p := strings.TrimPrefix(target, install.moduleName) 182 p = filepath.Join(install.srcRoot, p) 183 184 return p 185 } 186 187 func (install *PleaseGoInstall) parseImportConfig() error { 188 install.compiledPackages = map[string]string{ 189 "unsafe": "", // Not sure how many other packages like this I need to handle 190 "C": "", // Pseudo-package for cgo symbols 191 "embed": "", // Another psudo package 192 } 193 194 if install.importConfig != "" { 195 f, err := os.Open(install.importConfig) 196 if err != nil { 197 return fmt.Errorf("failed to open import config: %w", err) 198 } 199 defer f.Close() 200 201 importCfg := bufio.NewScanner(f) 202 for importCfg.Scan() { 203 line := importCfg.Text() 204 if strings.HasPrefix(line, "#") { 205 continue 206 } 207 parts := strings.Split(strings.TrimPrefix(line, "packagefile "), "=") 208 install.compiledPackages[parts[0]] = parts[1] 209 } 210 } 211 return nil 212 } 213 214 func checkCycle(path []string, next string) ([]string, error) { 215 for i, p := range path { 216 if p == next { 217 return nil, fmt.Errorf("package cycle detected: \n%s", strings.Join(append(path[i:], next), "\n ->")) 218 } 219 } 220 221 return append(path, next), nil 222 } 223 224 func (install *PleaseGoInstall) importDir(target string) (*build.Package, error) { 225 dir := filepath.Join(os.Getenv("TMP_DIR"), install.pkgDir(target)) 226 return install.buildContext.ImportDir(dir, build.ImportComment) 227 } 228 229 func (install *PleaseGoInstall) compile(from []string, target string) error { 230 if _, done := install.compiledPackages[target]; done { 231 return nil 232 } 233 fmt.Fprintf(os.Stderr, "Compiling package %s from %v\n", target, from) 234 235 from, err := checkCycle(from, target) 236 if err != nil { 237 return err 238 } 239 240 pkg, err := install.importDir(target) 241 if err != nil { 242 return err 243 } 244 245 for _, i := range pkg.Imports { 246 err := install.compile(from, i) 247 if err != nil { 248 if strings.Contains(err.Error(), "cannot find package") { 249 // Go will fail to find this import and provide a much better message than we can 250 continue 251 } 252 return err 253 } 254 } 255 256 err = install.compilePackage(target, pkg) 257 if err != nil { 258 return err 259 } 260 return nil 261 } 262 263 func (install *PleaseGoInstall) prepareDirectories(workDir, out string) error { 264 if err := install.tc.Exec.Run("mkdir -p %s", workDir); err != nil { 265 return err 266 } 267 return install.tc.Exec.Run("mkdir -p %s", filepath.Dir(out)) 268 } 269 270 // outPath returns the path to the .a for a given package. Unlike go build, please_go install will always output to 271 // the same location regardless of if the package matches the package dir base e.g. example.com/foo will always produce 272 // example.com/foo/foo.a no matter what the package under there is named. 273 // 274 // We can get away with this because we don't compile tests so there must be exactly one package per directory. 275 func outPath(outDir, target string) string { 276 dirName := filepath.Base(target) 277 return filepath.Join(outDir, filepath.Dir(target), dirName, dirName+".a") 278 } 279 280 func writeEmbedConfig(pkg *build.Package, path string) error { 281 cfg := &embed.Cfg{ 282 Patterns: map[string][]string{}, 283 Files: map[string]string{}, 284 } 285 286 if err := cfg.AddPackage(pkg); err != nil { 287 return err 288 } 289 data, err := json.Marshal(cfg) 290 if err != nil { 291 return err 292 } 293 294 return os.WriteFile(path, data, 0666) 295 } 296 297 func prefixPaths(paths []string, dir string) []string { 298 newPaths := make([]string, len(paths)) 299 for i, path := range paths { 300 newPaths[i] = filepath.Join(dir, path) 301 } 302 return newPaths 303 } 304 305 func (install *PleaseGoInstall) compilePackage(target string, pkg *build.Package) error { 306 if len(pkg.GoFiles)+len(pkg.CgoFiles) == 0 { 307 return nil 308 } 309 310 out := outPath(install.outDir, target) 311 workDir := filepath.Join(os.Getenv("TMP_DIR"), baseWorkDir, install.pkgDir(target)) 312 313 if err := install.prepareDirectories(workDir, out); err != nil { 314 return fmt.Errorf("failed to prepare directories for %s: %w", target, err) 315 } 316 317 goFiles := prefixPaths(pkg.GoFiles, pkg.Dir) 318 objFiles := []string{} 319 ldFlags := []string{} 320 321 cgoFiles := prefixPaths(pkg.CgoFiles, pkg.Dir) 322 if len(cgoFiles) > 0 { 323 cFlags := pkg.CgoCFLAGS 324 ldFlags = append(ldFlags, pkg.CgoLDFLAGS...) 325 326 // Collect pkg-config flags. 327 if len(pkg.CgoPkgConfig) > 0 { 328 pkgConfCFlags, err := install.tc.PkgConfigCFlags(pkg.CgoPkgConfig) 329 if err != nil { 330 return err 331 } 332 333 cFlags = append(cFlags, pkgConfCFlags...) 334 335 pkgConfLDFlags, err := install.tc.PkgConfigLDFlags(pkg.CgoPkgConfig) 336 if err != nil { 337 return err 338 } 339 340 ldFlags = append(ldFlags, pkgConfLDFlags...) 341 if len(pkgConfLDFlags) > 0 { 342 fmt.Fprintf(os.Stderr, "------ ***** ------ ld flags for %s: %s\n", target, strings.Join(pkgConfLDFlags, " ")) 343 } 344 } 345 346 // Append C flags passed to the program. 347 if f := install.additionalCFlags; f != "" { 348 cFlags = append(cFlags, f) 349 } 350 351 cgoGoWorkFiles, cgoCWorkFiles, err := install.tc.CGO(pkg.Dir, workDir, cFlags, cgoFiles) 352 if err != nil { 353 return err 354 } 355 goFiles = append(goFiles, cgoGoWorkFiles...) 356 357 // Compile the C files generated by the GCO command above. 358 cgoCObjFiles, err := install.tc.CCompile(workDir, workDir, cgoCWorkFiles, append(cFlags, "-I"+pkg.Dir)) 359 if err != nil { 360 return err 361 } 362 objFiles = append(objFiles, cgoCObjFiles...) 363 364 // Compile C files in original source code. 365 cFiles := prefixPaths(pkg.CFiles, pkg.Dir) 366 if len(cFiles) > 0 { 367 cObjFiles, err := install.tc.CCompile(pkg.Dir, workDir, cFiles, append(cFlags, "-I"+workDir)) 368 if err != nil { 369 return err 370 } 371 objFiles = append(objFiles, cObjFiles...) 372 } 373 374 // Compile CXX files in original source code. 375 ccFiles := prefixPaths(pkg.CXXFiles, pkg.Dir) 376 if len(ccFiles) > 0 { 377 ccObjFiles, err := install.tc.CCompile(pkg.Dir, workDir, ccFiles, append(append(cFlags, pkg.CgoCXXFLAGS...), "-I"+workDir)) 378 if err != nil { 379 return err 380 } 381 objFiles = append(objFiles, ccObjFiles...) 382 } 383 } 384 385 embedConfig := "" 386 if len(pkg.EmbedPatterns) > 0 { 387 embedConfig = filepath.Join(workDir, "embed.cfg") 388 if err := writeEmbedConfig(pkg, embedConfig); err != nil { 389 return fmt.Errorf("failed to write embed config: %v", err) 390 } 391 } 392 393 importPath := target 394 if pkg.IsCommand() { 395 importPath = "main" 396 } 397 398 asmFiles := prefixPaths(pkg.SFiles, pkg.Dir) 399 if len(asmFiles) > 0 { 400 asmH, symabis, err := install.tc.Symabis(importPath, pkg.Dir, workDir, asmFiles) 401 if err != nil { 402 return err 403 } 404 405 if err := install.tc.GoAsmCompile(importPath, install.importConfig, out, install.trimPath, embedConfig, goFiles, asmH, symabis); err != nil { 406 return err 407 } 408 409 asmObjFiles, err := install.tc.Asm(importPath, pkg.Dir, workDir, install.trimPath, asmFiles) 410 if err != nil { 411 return err 412 } 413 414 objFiles = append(objFiles, asmObjFiles...) 415 } else if err := install.tc.GoCompile(pkg.Dir, importPath, install.importConfig, out, install.trimPath, embedConfig, goFiles); err != nil { 416 return err 417 } 418 419 if len(objFiles) > 0 { 420 if err := install.tc.Pack(workDir, out, objFiles); err != nil { 421 return err 422 } 423 } 424 425 if err := install.tc.Exec.Run("echo \"packagefile %s=%s\" >> %s", target, out, install.importConfig); err != nil { 426 return err 427 } 428 429 install.collectedLdFlags = append(install.collectedLdFlags, ldFlags...) 430 431 install.compiledPackages[target] = out 432 return nil 433 }