cross-compile.go 12 KB

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