github.com/artpar/rclone@v1.67.3/bin/cross-compile.go (about) 1 //go:build ignore 2 3 // Cross compile rclone - in go because I hate bash ;-) 4 5 package main 6 7 import ( 8 "flag" 9 "fmt" 10 "log" 11 "os" 12 "os/exec" 13 "path" 14 "path/filepath" 15 "regexp" 16 "runtime" 17 "sort" 18 "strings" 19 "sync" 20 "text/template" 21 "time" 22 ) 23 24 var ( 25 // Flags 26 debug = flag.Bool("d", false, "Print commands instead of running them") 27 parallel = flag.Int("parallel", runtime.NumCPU(), "Number of commands to run in parallel") 28 copyAs = flag.String("release", "", "Make copies of the releases with this name") 29 gitLog = flag.String("git-log", "", "git log to include as well") 30 include = flag.String("include", "^.*$", "os/arch regexp to include") 31 exclude = flag.String("exclude", "^$", "os/arch regexp to exclude") 32 cgo = flag.Bool("cgo", false, "Use cgo for the build") 33 noClean = flag.Bool("no-clean", false, "Don't clean the build directory before running") 34 tags = flag.String("tags", "", "Space separated list of build tags") 35 buildmode = flag.String("buildmode", "", "Passed to go build -buildmode flag") 36 compileOnly = flag.Bool("compile-only", false, "Just build the binary, not the zip") 37 extraEnv = flag.String("env", "", "comma separated list of VAR=VALUE env vars to set") 38 macOSSDK = flag.String("macos-sdk", "", "macOS SDK to use") 39 macOSArch = flag.String("macos-arch", "", "macOS arch to use") 40 extraCgoCFlags = flag.String("cgo-cflags", "", "extra CGO_CFLAGS") 41 extraCgoLdFlags = flag.String("cgo-ldflags", "", "extra CGO_LDFLAGS") 42 ) 43 44 // GOOS/GOARCH pairs we build for 45 // 46 // If the GOARCH contains a - it is a synthetic arch with more parameters 47 var osarches = []string{ 48 "windows/386", 49 "windows/amd64", 50 "windows/arm64", 51 "darwin/amd64", 52 "darwin/arm64", 53 "linux/386", 54 "linux/amd64", 55 "linux/arm", 56 "linux/arm-v6", 57 "linux/arm-v7", 58 "linux/arm64", 59 "linux/mips", 60 "linux/mipsle", 61 "freebsd/386", 62 "freebsd/amd64", 63 "freebsd/arm", 64 "freebsd/arm-v6", 65 "freebsd/arm-v7", 66 "netbsd/386", 67 "netbsd/amd64", 68 "netbsd/arm", 69 "netbsd/arm-v6", 70 "netbsd/arm-v7", 71 "openbsd/386", 72 "openbsd/amd64", 73 "plan9/386", 74 "plan9/amd64", 75 "solaris/amd64", 76 "js/wasm", 77 } 78 79 // Special environment flags for a given arch 80 var archFlags = map[string][]string{ 81 "386": {"GO386=softfloat"}, 82 "mips": {"GOMIPS=softfloat"}, 83 "mipsle": {"GOMIPS=softfloat"}, 84 "arm": {"GOARM=5"}, 85 "arm-v6": {"GOARM=6"}, 86 "arm-v7": {"GOARM=7"}, 87 } 88 89 // Map Go architectures to NFPM architectures 90 // Any missing are passed straight through 91 var goarchToNfpm = map[string]string{ 92 "arm": "arm5", 93 "arm-v6": "arm6", 94 "arm-v7": "arm7", 95 } 96 97 // runEnv - run a shell command with env 98 func runEnv(args, env []string) error { 99 if *debug { 100 args = append([]string{"echo"}, args...) 101 } 102 cmd := exec.Command(args[0], args[1:]...) 103 if env != nil { 104 cmd.Env = append(os.Environ(), env...) 105 } 106 if *debug { 107 log.Printf("args = %v, env = %v\n", args, cmd.Env) 108 } 109 out, err := cmd.CombinedOutput() 110 if err != nil { 111 log.Print("----------------------------") 112 log.Printf("Failed to run %v: %v", args, err) 113 log.Printf("Command output was:\n%s", out) 114 log.Print("----------------------------") 115 } 116 return err 117 } 118 119 // run a shell command 120 func run(args ...string) { 121 err := runEnv(args, nil) 122 if err != nil { 123 log.Fatalf("Exiting after error: %v", err) 124 } 125 } 126 127 // chdir or die 128 func chdir(dir string) { 129 err := os.Chdir(dir) 130 if err != nil { 131 log.Fatalf("Couldn't cd into %q: %v", dir, err) 132 } 133 } 134 135 // substitute data from go template file in to file out 136 func substitute(inFile, outFile string, data interface{}) { 137 t, err := template.ParseFiles(inFile) 138 if err != nil { 139 log.Fatalf("Failed to read template file %q: %v", inFile, err) 140 } 141 out, err := os.Create(outFile) 142 if err != nil { 143 log.Fatalf("Failed to create output file %q: %v", outFile, err) 144 } 145 defer func() { 146 err := out.Close() 147 if err != nil { 148 log.Fatalf("Failed to close output file %q: %v", outFile, err) 149 } 150 }() 151 err = t.Execute(out, data) 152 if err != nil { 153 log.Fatalf("Failed to substitute template file %q: %v", inFile, err) 154 } 155 } 156 157 // build the zip package return its name 158 func buildZip(dir string) string { 159 // Now build the zip 160 run("cp", "-a", "../MANUAL.txt", filepath.Join(dir, "README.txt")) 161 run("cp", "-a", "../MANUAL.html", filepath.Join(dir, "README.html")) 162 run("cp", "-a", "../rclone.1", dir) 163 if *gitLog != "" { 164 run("cp", "-a", *gitLog, dir) 165 } 166 zip := dir + ".zip" 167 run("zip", "-r9", zip, dir) 168 return zip 169 } 170 171 // Build .deb and .rpm packages 172 // 173 // It returns a list of artifacts it has made 174 func buildDebAndRpm(dir, version, goarch string) []string { 175 // Make internal version number acceptable to .deb and .rpm 176 pkgVersion := version[1:] 177 pkgVersion = strings.ReplaceAll(pkgVersion, "β", "-beta") 178 pkgVersion = strings.ReplaceAll(pkgVersion, "-", ".") 179 nfpmArch, ok := goarchToNfpm[goarch] 180 if !ok { 181 nfpmArch = goarch 182 } 183 184 // Make nfpm.yaml from the template 185 substitute("../bin/nfpm.yaml", path.Join(dir, "nfpm.yaml"), map[string]string{ 186 "Version": pkgVersion, 187 "Arch": nfpmArch, 188 }) 189 190 // build them 191 var artifacts []string 192 for _, pkg := range []string{".deb", ".rpm"} { 193 artifact := dir + pkg 194 run("bash", "-c", "cd "+dir+" && nfpm -f nfpm.yaml pkg -t ../"+artifact) 195 artifacts = append(artifacts, artifact) 196 } 197 198 return artifacts 199 } 200 201 // Trip a version suffix off the arch if present 202 func stripVersion(goarch string) string { 203 i := strings.Index(goarch, "-") 204 if i < 0 { 205 return goarch 206 } 207 return goarch[:i] 208 } 209 210 // run the command returning trimmed output 211 func runOut(command ...string) string { 212 out, err := exec.Command(command[0], command[1:]...).Output() 213 if err != nil { 214 log.Fatalf("Failed to run %q: %v", command, err) 215 } 216 return strings.TrimSpace(string(out)) 217 } 218 219 // Generate Windows resource system object file (.syso), which can be picked 220 // up by the following go build for embedding version information and icon 221 // resources into the executable. 222 func generateResourceWindows(version, arch string) func() { 223 sysoPath := fmt.Sprintf("../resource_windows_%s.syso", arch) // Use explicit destination filename, even though it should be same as default, so that we are sure we have the correct reference to it 224 if err := os.Remove(sysoPath); !os.IsNotExist(err) { 225 // Note: This one we choose to treat as fatal, to avoid any risk of picking up an old .syso file without noticing. 226 log.Fatalf("Failed to remove existing Windows %s resource system object file %s: %v", arch, sysoPath, err) 227 } 228 args := []string{"go", "run", "../bin/resource_windows.go", "-arch", arch, "-version", version, "-syso", sysoPath} 229 if err := runEnv(args, nil); err != nil { 230 log.Printf("Warning: Couldn't generate Windows %s resource system object file, binaries will not have version information or icon embedded", arch) 231 return nil 232 } 233 if _, err := os.Stat(sysoPath); err != nil { 234 log.Printf("Warning: Couldn't find generated Windows %s resource system object file, binaries will not have version information or icon embedded", arch) 235 return nil 236 } 237 return func() { 238 if err := os.Remove(sysoPath); err != nil && !os.IsNotExist(err) { 239 log.Printf("Warning: Couldn't remove generated Windows %s resource system object file %s: %v. Please remove it manually.", arch, sysoPath, err) 240 } 241 } 242 } 243 244 // build the binary in dir returning success or failure 245 func compileArch(version, goos, goarch, dir string) bool { 246 log.Printf("Compiling %s/%s into %s", goos, goarch, dir) 247 goarchBase := stripVersion(goarch) 248 output := filepath.Join(dir, "rclone") 249 if goos == "windows" { 250 output += ".exe" 251 if cleanupFn := generateResourceWindows(version, goarchBase); cleanupFn != nil { 252 defer cleanupFn() 253 } 254 } 255 err := os.MkdirAll(dir, 0777) 256 if err != nil { 257 log.Fatalf("Failed to mkdir: %v", err) 258 } 259 args := []string{ 260 "go", "build", 261 "--ldflags", "-s -X github.com/artpar/rclone/fs.Version=" + version, 262 "-trimpath", 263 "-o", output, 264 "-tags", *tags, 265 } 266 if *buildmode != "" { 267 args = append(args, 268 "-buildmode", *buildmode, 269 ) 270 } 271 args = append(args, 272 "..", 273 ) 274 env := []string{ 275 "GOOS=" + goos, 276 "GOARCH=" + goarchBase, 277 } 278 if *extraEnv != "" { 279 env = append(env, strings.Split(*extraEnv, ",")...) 280 } 281 var ( 282 cgoCFlags []string 283 cgoLdFlags []string 284 ) 285 if *macOSSDK != "" { 286 flag := "-isysroot " + runOut("xcrun", "--sdk", *macOSSDK, "--show-sdk-path") 287 cgoCFlags = append(cgoCFlags, flag) 288 cgoLdFlags = append(cgoLdFlags, flag) 289 } 290 if *macOSArch != "" { 291 flag := "-arch " + *macOSArch 292 cgoCFlags = append(cgoCFlags, flag) 293 cgoLdFlags = append(cgoLdFlags, flag) 294 } 295 if *extraCgoCFlags != "" { 296 cgoCFlags = append(cgoCFlags, *extraCgoCFlags) 297 } 298 if *extraCgoLdFlags != "" { 299 cgoLdFlags = append(cgoLdFlags, *extraCgoLdFlags) 300 } 301 if len(cgoCFlags) > 0 { 302 env = append(env, "CGO_CFLAGS="+strings.Join(cgoCFlags, " ")) 303 } 304 if len(cgoLdFlags) > 0 { 305 env = append(env, "CGO_LDFLAGS="+strings.Join(cgoLdFlags, " ")) 306 } 307 if !*cgo { 308 env = append(env, "CGO_ENABLED=0") 309 } else { 310 env = append(env, "CGO_ENABLED=1") 311 } 312 if flags, ok := archFlags[goarch]; ok { 313 env = append(env, flags...) 314 } 315 err = runEnv(args, env) 316 if err != nil { 317 log.Printf("Error compiling %s/%s: %v", goos, goarch, err) 318 return false 319 } 320 if !*compileOnly { 321 if goos != "js" { 322 artifacts := []string{buildZip(dir)} 323 // build a .deb and .rpm if appropriate 324 if goos == "linux" { 325 artifacts = append(artifacts, buildDebAndRpm(dir, version, goarch)...) 326 } 327 if *copyAs != "" { 328 for _, artifact := range artifacts { 329 run("ln", artifact, strings.Replace(artifact, "-"+version, "-"+*copyAs, 1)) 330 } 331 } 332 } 333 // tidy up 334 run("rm", "-rf", dir) 335 } 336 log.Printf("Done compiling %s/%s", goos, goarch) 337 return true 338 } 339 340 func compile(version string) { 341 start := time.Now() 342 wg := new(sync.WaitGroup) 343 run := make(chan func(), *parallel) 344 for i := 0; i < *parallel; i++ { 345 wg.Add(1) 346 go func() { 347 defer wg.Done() 348 for f := range run { 349 f() 350 } 351 }() 352 } 353 includeRe, err := regexp.Compile(*include) 354 if err != nil { 355 log.Fatalf("Bad -include regexp: %v", err) 356 } 357 excludeRe, err := regexp.Compile(*exclude) 358 if err != nil { 359 log.Fatalf("Bad -exclude regexp: %v", err) 360 } 361 compiled := 0 362 var failuresMu sync.Mutex 363 var failures []string 364 for _, osarch := range osarches { 365 if excludeRe.MatchString(osarch) || !includeRe.MatchString(osarch) { 366 continue 367 } 368 parts := strings.Split(osarch, "/") 369 if len(parts) != 2 { 370 log.Fatalf("Bad osarch %q", osarch) 371 } 372 goos, goarch := parts[0], parts[1] 373 userGoos := goos 374 if goos == "darwin" { 375 userGoos = "osx" 376 } 377 dir := filepath.Join("rclone-" + version + "-" + userGoos + "-" + goarch) 378 run <- func() { 379 if !compileArch(version, goos, goarch, dir) { 380 failuresMu.Lock() 381 failures = append(failures, goos+"/"+goarch) 382 failuresMu.Unlock() 383 } 384 } 385 compiled++ 386 } 387 close(run) 388 wg.Wait() 389 log.Printf("Compiled %d arches in %v", compiled, time.Since(start)) 390 if len(failures) > 0 { 391 sort.Strings(failures) 392 log.Printf("%d compile failures:\n %s\n", len(failures), strings.Join(failures, "\n ")) 393 os.Exit(1) 394 } 395 } 396 397 func main() { 398 flag.Parse() 399 args := flag.Args() 400 if len(args) != 1 { 401 log.Fatalf("Syntax: %s <version>", os.Args[0]) 402 } 403 version := args[0] 404 if !*noClean { 405 run("rm", "-rf", "build") 406 run("mkdir", "build") 407 } 408 chdir("build") 409 err := os.WriteFile("version.txt", []byte(fmt.Sprintf("rclone %s\n", version)), 0666) 410 if err != nil { 411 log.Fatalf("Couldn't write version.txt: %v", err) 412 } 413 compile(version) 414 }