1// Copyright (C) 2014 The Syncthing Authors.
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this file,
5// You can obtain one at https://mozilla.org/MPL/2.0/.
6
7// +build ignore
8
9package main
10
11import (
12	"archive/tar"
13	"archive/zip"
14	"bytes"
15	"compress/flate"
16	"compress/gzip"
17	"encoding/json"
18	"errors"
19	"flag"
20	"fmt"
21	"io"
22	"io/ioutil"
23	"log"
24	"os"
25	"os/exec"
26	"os/user"
27	"path/filepath"
28	"regexp"
29	"runtime"
30	"strconv"
31	"strings"
32	"text/template"
33	"time"
34)
35
36var (
37	goarch         string
38	goos           string
39	noupgrade      bool
40	version        string
41	goCmd          string
42	race           bool
43	debug          = os.Getenv("BUILDDEBUG") != ""
44	extraTags      string
45	installSuffix  string
46	pkgdir         string
47	cc             string
48	run            string
49	benchRun       string
50	debugBinary    bool
51	coverage       bool
52	long           bool
53	timeout        = "120s"
54	longTimeout    = "600s"
55	numVersions    = 5
56	withNextGenGUI = os.Getenv("BUILD_NEXT_GEN_GUI") != ""
57)
58
59type target struct {
60	name              string
61	debname           string
62	debdeps           []string
63	debpre            string
64	description       string
65	buildPkgs         []string
66	binaryName        string
67	archiveFiles      []archiveFile
68	systemdService    string
69	installationFiles []archiveFile
70	tags              []string
71}
72
73type archiveFile struct {
74	src  string
75	dst  string
76	perm os.FileMode
77}
78
79var targets = map[string]target{
80	"all": {
81		// Only valid for the "build" and "install" commands as it lacks all
82		// the archive creation stuff. buildPkgs gets filled out in init()
83		tags: []string{"purego"},
84	},
85	"syncthing": {
86		// The default target for "build", "install", "tar", "zip", "deb", etc.
87		name:        "syncthing",
88		debname:     "syncthing",
89		debdeps:     []string{"libc6", "procps"},
90		description: "Open Source Continuous File Synchronization",
91		buildPkgs:   []string{"github.com/syncthing/syncthing/cmd/syncthing"},
92		binaryName:  "syncthing", // .exe will be added automatically for Windows builds
93		archiveFiles: []archiveFile{
94			{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
95			{src: "README.md", dst: "README.txt", perm: 0644},
96			{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
97			{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
98			// All files from etc/ and extra/ added automatically in init().
99		},
100		systemdService: "syncthing@*.service",
101		installationFiles: []archiveFile{
102			{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
103			{src: "README.md", dst: "deb/usr/share/doc/syncthing/README.txt", perm: 0644},
104			{src: "LICENSE", dst: "deb/usr/share/doc/syncthing/LICENSE.txt", perm: 0644},
105			{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing/AUTHORS.txt", perm: 0644},
106			{src: "man/syncthing.1", dst: "deb/usr/share/man/man1/syncthing.1", perm: 0644},
107			{src: "man/syncthing-config.5", dst: "deb/usr/share/man/man5/syncthing-config.5", perm: 0644},
108			{src: "man/syncthing-stignore.5", dst: "deb/usr/share/man/man5/syncthing-stignore.5", perm: 0644},
109			{src: "man/syncthing-device-ids.7", dst: "deb/usr/share/man/man7/syncthing-device-ids.7", perm: 0644},
110			{src: "man/syncthing-event-api.7", dst: "deb/usr/share/man/man7/syncthing-event-api.7", perm: 0644},
111			{src: "man/syncthing-faq.7", dst: "deb/usr/share/man/man7/syncthing-faq.7", perm: 0644},
112			{src: "man/syncthing-networking.7", dst: "deb/usr/share/man/man7/syncthing-networking.7", perm: 0644},
113			{src: "man/syncthing-rest-api.7", dst: "deb/usr/share/man/man7/syncthing-rest-api.7", perm: 0644},
114			{src: "man/syncthing-security.7", dst: "deb/usr/share/man/man7/syncthing-security.7", perm: 0644},
115			{src: "man/syncthing-versioning.7", dst: "deb/usr/share/man/man7/syncthing-versioning.7", perm: 0644},
116			{src: "etc/linux-systemd/system/syncthing@.service", dst: "deb/lib/systemd/system/syncthing@.service", perm: 0644},
117			{src: "etc/linux-systemd/system/syncthing-resume.service", dst: "deb/lib/systemd/system/syncthing-resume.service", perm: 0644},
118			{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0644},
119			{src: "etc/linux-sysctl/30-syncthing.conf", dst: "deb/usr/lib/sysctl.d/30-syncthing.conf", perm: 0644},
120			{src: "etc/firewall-ufw/syncthing", dst: "deb/etc/ufw/applications.d/syncthing", perm: 0644},
121			{src: "etc/linux-desktop/syncthing-start.desktop", dst: "deb/usr/share/applications/syncthing-start.desktop", perm: 0644},
122			{src: "etc/linux-desktop/syncthing-ui.desktop", dst: "deb/usr/share/applications/syncthing-ui.desktop", perm: 0644},
123			{src: "assets/logo-32.png", dst: "deb/usr/share/icons/hicolor/32x32/apps/syncthing.png", perm: 0644},
124			{src: "assets/logo-64.png", dst: "deb/usr/share/icons/hicolor/64x64/apps/syncthing.png", perm: 0644},
125			{src: "assets/logo-128.png", dst: "deb/usr/share/icons/hicolor/128x128/apps/syncthing.png", perm: 0644},
126			{src: "assets/logo-256.png", dst: "deb/usr/share/icons/hicolor/256x256/apps/syncthing.png", perm: 0644},
127			{src: "assets/logo-512.png", dst: "deb/usr/share/icons/hicolor/512x512/apps/syncthing.png", perm: 0644},
128			{src: "assets/logo-only.svg", dst: "deb/usr/share/icons/hicolor/scalable/apps/syncthing.svg", perm: 0644},
129		},
130	},
131	"stdiscosrv": {
132		name:        "stdiscosrv",
133		debname:     "syncthing-discosrv",
134		debdeps:     []string{"libc6"},
135		debpre:      "cmd/stdiscosrv/scripts/preinst",
136		description: "Syncthing Discovery Server",
137		buildPkgs:   []string{"github.com/syncthing/syncthing/cmd/stdiscosrv"},
138		binaryName:  "stdiscosrv", // .exe will be added automatically for Windows builds
139		archiveFiles: []archiveFile{
140			{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
141			{src: "cmd/stdiscosrv/README.md", dst: "README.txt", perm: 0644},
142			{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
143			{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
144		},
145		systemdService: "cmd/stdiscosrv/etc/linux-systemd/stdiscosrv.service",
146		installationFiles: []archiveFile{
147			{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
148			{src: "cmd/stdiscosrv/README.md", dst: "deb/usr/share/doc/syncthing-discosrv/README.txt", perm: 0644},
149			{src: "LICENSE", dst: "deb/usr/share/doc/syncthing-discosrv/LICENSE.txt", perm: 0644},
150			{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-discosrv/AUTHORS.txt", perm: 0644},
151			{src: "man/stdiscosrv.1", dst: "deb/usr/share/man/man1/stdiscosrv.1", perm: 0644},
152			{src: "cmd/stdiscosrv/etc/linux-systemd/default", dst: "deb/etc/default/syncthing-discosrv", perm: 0644},
153			{src: "cmd/stdiscosrv/etc/firewall-ufw/stdiscosrv", dst: "deb/etc/ufw/applications.d/stdiscosrv", perm: 0644},
154		},
155		tags: []string{"purego"},
156	},
157	"strelaysrv": {
158		name:        "strelaysrv",
159		debname:     "syncthing-relaysrv",
160		debdeps:     []string{"libc6"},
161		debpre:      "cmd/strelaysrv/scripts/preinst",
162		description: "Syncthing Relay Server",
163		buildPkgs:   []string{"github.com/syncthing/syncthing/cmd/strelaysrv"},
164		binaryName:  "strelaysrv", // .exe will be added automatically for Windows builds
165		archiveFiles: []archiveFile{
166			{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
167			{src: "cmd/strelaysrv/README.md", dst: "README.txt", perm: 0644},
168			{src: "cmd/strelaysrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
169			{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
170			{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
171		},
172		systemdService: "cmd/strelaysrv/etc/linux-systemd/strelaysrv.service",
173		installationFiles: []archiveFile{
174			{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
175			{src: "cmd/strelaysrv/README.md", dst: "deb/usr/share/doc/syncthing-relaysrv/README.txt", perm: 0644},
176			{src: "cmd/strelaysrv/LICENSE", dst: "deb/usr/share/doc/syncthing-relaysrv/LICENSE.txt", perm: 0644},
177			{src: "LICENSE", dst: "deb/usr/share/doc/syncthing-relaysrv/LICENSE.txt", perm: 0644},
178			{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-relaysrv/AUTHORS.txt", perm: 0644},
179			{src: "man/strelaysrv.1", dst: "deb/usr/share/man/man1/strelaysrv.1", perm: 0644},
180			{src: "cmd/strelaysrv/etc/linux-systemd/default", dst: "deb/etc/default/syncthing-relaysrv", perm: 0644},
181			{src: "cmd/strelaysrv/etc/firewall-ufw/strelaysrv", dst: "deb/etc/ufw/applications.d/strelaysrv", perm: 0644},
182		},
183	},
184	"strelaypoolsrv": {
185		name:        "strelaypoolsrv",
186		debname:     "syncthing-relaypoolsrv",
187		debdeps:     []string{"libc6"},
188		description: "Syncthing Relay Pool Server",
189		buildPkgs:   []string{"github.com/syncthing/syncthing/cmd/strelaypoolsrv"},
190		binaryName:  "strelaypoolsrv", // .exe will be added automatically for Windows builds
191		archiveFiles: []archiveFile{
192			{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
193			{src: "cmd/strelaypoolsrv/README.md", dst: "README.txt", perm: 0644},
194			{src: "cmd/strelaypoolsrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
195			{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
196		},
197		installationFiles: []archiveFile{
198			{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
199			{src: "cmd/strelaypoolsrv/README.md", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/README.txt", perm: 0644},
200			{src: "cmd/strelaypoolsrv/LICENSE", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/LICENSE.txt", perm: 0644},
201			{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/AUTHORS.txt", perm: 0644},
202		},
203	},
204}
205
206func initTargets() {
207	all := targets["all"]
208	pkgs, _ := filepath.Glob("cmd/*")
209	for _, pkg := range pkgs {
210		pkg = filepath.Base(pkg)
211		if strings.HasPrefix(pkg, ".") {
212			// ignore dotfiles
213			continue
214		}
215		if noupgrade && pkg == "stupgrades" {
216			continue
217		}
218		all.buildPkgs = append(all.buildPkgs, fmt.Sprintf("github.com/syncthing/syncthing/cmd/%s", pkg))
219	}
220	targets["all"] = all
221
222	// The "syncthing" target includes a few more files found in the "etc"
223	// and "extra" dirs.
224	syncthingPkg := targets["syncthing"]
225	for _, file := range listFiles("etc") {
226		syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0644})
227	}
228	for _, file := range listFiles("extra") {
229		syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0644})
230	}
231	for _, file := range listFiles("extra") {
232		syncthingPkg.installationFiles = append(syncthingPkg.installationFiles, archiveFile{src: file, dst: "deb/usr/share/doc/syncthing/" + filepath.Base(file), perm: 0644})
233	}
234	targets["syncthing"] = syncthingPkg
235}
236
237func main() {
238	log.SetFlags(0)
239
240	parseFlags()
241
242	if debug {
243		t0 := time.Now()
244		defer func() {
245			log.Println("... build completed in", time.Since(t0))
246		}()
247	}
248
249	initTargets()
250
251	// Invoking build.go with no parameters at all builds everything (incrementally),
252	// which is what you want for maximum error checking during development.
253	if flag.NArg() == 0 {
254		runCommand("install", targets["all"])
255	} else {
256		// with any command given but not a target, the target is
257		// "syncthing". So "go run build.go install" is "go run build.go install
258		// syncthing" etc.
259		targetName := "syncthing"
260		if flag.NArg() > 1 {
261			targetName = flag.Arg(1)
262		}
263		target, ok := targets[targetName]
264		if !ok {
265			log.Fatalln("Unknown target", target)
266		}
267
268		runCommand(flag.Arg(0), target)
269	}
270}
271
272func runCommand(cmd string, target target) {
273	var tags []string
274	if noupgrade {
275		tags = []string{"noupgrade"}
276	}
277	tags = append(tags, strings.Fields(extraTags)...)
278
279	switch cmd {
280	case "install":
281		install(target, tags)
282		metalintShort()
283
284	case "build":
285		build(target, tags)
286
287	case "test":
288		test(strings.Fields(extraTags), "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
289
290	case "bench":
291		bench(strings.Fields(extraTags), "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
292
293	case "integration":
294		integration(false)
295
296	case "integrationbench":
297		integration(true)
298
299	case "assets":
300		rebuildAssets()
301
302	case "proto":
303		proto()
304
305	case "testmocks":
306		testmocks()
307
308	case "translate":
309		translate()
310
311	case "transifex":
312		transifex()
313
314	case "tar":
315		buildTar(target, tags)
316
317	case "zip":
318		buildZip(target, tags)
319
320	case "deb":
321		buildDeb(target)
322
323	case "vet":
324		metalintShort()
325
326	case "lint":
327		metalintShort()
328
329	case "metalint":
330		metalint()
331
332	case "version":
333		fmt.Println(getVersion())
334
335	case "changelog":
336		vers, err := currentAndLatestVersions(numVersions)
337		if err != nil {
338			log.Fatal(err)
339		}
340		for _, ver := range vers {
341			underline := strings.Repeat("=", len(ver))
342			msg, err := tagMessage(ver)
343			if err != nil {
344				log.Fatal(err)
345			}
346			fmt.Printf("%s\n%s\n\n%s\n\n", ver, underline, msg)
347		}
348
349	default:
350		log.Fatalf("Unknown command %q", cmd)
351	}
352}
353
354func parseFlags() {
355	flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
356	flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
357	flag.StringVar(&goCmd, "gocmd", "go", "Specify `go` command")
358	flag.BoolVar(&noupgrade, "no-upgrade", noupgrade, "Disable upgrade functionality")
359	flag.StringVar(&version, "version", getVersion(), "Set compiled in version string")
360	flag.BoolVar(&race, "race", race, "Use race detector")
361	flag.StringVar(&extraTags, "tags", extraTags, "Extra tags, space separated")
362	flag.StringVar(&installSuffix, "installsuffix", installSuffix, "Install suffix, optional")
363	flag.StringVar(&pkgdir, "pkgdir", "", "Set -pkgdir parameter for `go build`")
364	flag.StringVar(&cc, "cc", os.Getenv("CC"), "Set CC environment variable for `go build`")
365	flag.BoolVar(&debugBinary, "debug-binary", debugBinary, "Create unoptimized binary to use with delve, set -gcflags='-N -l' and omit -ldflags")
366	flag.BoolVar(&coverage, "coverage", coverage, "Write coverage profile of tests to coverage.txt")
367	flag.BoolVar(&long, "long", long, "Run tests without the -short flag")
368	flag.IntVar(&numVersions, "num-versions", numVersions, "Number of versions for changelog command")
369	flag.StringVar(&run, "run", "", "Specify which tests to run")
370	flag.StringVar(&benchRun, "bench", "", "Specify which benchmarks to run")
371	flag.BoolVar(&withNextGenGUI, "with-next-gen-gui", withNextGenGUI, "Also build 'newgui'")
372	flag.Parse()
373}
374
375func test(tags []string, pkgs ...string) {
376	lazyRebuildAssets()
377
378	tags = append(tags, "purego")
379	args := []string{"test", "-tags", strings.Join(tags, " ")}
380	if long {
381		timeout = longTimeout
382	} else {
383		args = append(args, "-short")
384	}
385	args = append(args, "-timeout", timeout)
386
387	if runtime.GOARCH == "amd64" {
388		switch runtime.GOOS {
389		case "darwin", "linux", "freebsd": // , "windows": # See https://github.com/golang/go/issues/27089
390			args = append(args, "-race")
391		}
392	}
393
394	if coverage {
395		args = append(args, "-covermode", "atomic", "-coverprofile", "coverage.txt", "-coverpkg", strings.Join(pkgs, ","))
396	}
397
398	args = append(args, runArgs()...)
399
400	runPrint(goCmd, append(args, pkgs...)...)
401}
402
403func bench(tags []string, pkgs ...string) {
404	lazyRebuildAssets()
405	args := append([]string{"test", "-run", "NONE", "-tags", strings.Join(tags, " ")}, benchArgs()...)
406	runPrint(goCmd, append(args, pkgs...)...)
407}
408
409func integration(bench bool) {
410	lazyRebuildAssets()
411	args := []string{"test", "-v", "-timeout", "60m", "-tags"}
412	tags := "purego,integration"
413	if bench {
414		tags += ",benchmark"
415	}
416	args = append(args, tags)
417	args = append(args, runArgs()...)
418	if bench {
419		if run == "" {
420			args = append(args, "-run", "Benchmark")
421		}
422		args = append(args, benchArgs()...)
423	}
424	args = append(args, "./test")
425	fmt.Println(args)
426	runPrint(goCmd, args...)
427}
428
429func runArgs() []string {
430	if run == "" {
431		return nil
432	}
433	return []string{"-run", run}
434}
435
436func benchArgs() []string {
437	if benchRun == "" {
438		return []string{"-bench", "."}
439	}
440	return []string{"-bench", benchRun}
441}
442
443func install(target target, tags []string) {
444	if (target.name == "syncthing" || target.name == "") && !withNextGenGUI {
445		log.Println("Notice: Next generation GUI will not be built; see --with-next-gen-gui.")
446	}
447
448	lazyRebuildAssets()
449
450	tags = append(target.tags, tags...)
451
452	cwd, err := os.Getwd()
453	if err != nil {
454		log.Fatal(err)
455	}
456	os.Setenv("GOBIN", filepath.Join(cwd, "bin"))
457
458	setBuildEnvVars()
459
460	// On Windows generate a special file which the Go compiler will
461	// automatically use when generating Windows binaries to set things like
462	// the file icon, version, etc.
463	if goos == "windows" {
464		sysoPath, err := shouldBuildSyso(cwd)
465		if err != nil {
466			log.Printf("Warning: Windows binaries will not have file information encoded: %v", err)
467		}
468		defer shouldCleanupSyso(sysoPath)
469	}
470
471	args := []string{"install", "-v"}
472	args = appendParameters(args, tags, target.buildPkgs...)
473	runPrint(goCmd, args...)
474}
475
476func build(target target, tags []string) {
477	if (target.name == "syncthing" || target.name == "") && !withNextGenGUI {
478		log.Println("Notice: Next generation GUI will not be built; see --with-next-gen-gui.")
479	}
480
481	lazyRebuildAssets()
482	tags = append(target.tags, tags...)
483
484	rmr(target.BinaryName())
485
486	setBuildEnvVars()
487
488	// On Windows generate a special file which the Go compiler will
489	// automatically use when generating Windows binaries to set things like
490	// the file icon, version, etc.
491	if goos == "windows" {
492		cwd, err := os.Getwd()
493		if err != nil {
494			log.Fatal(err)
495		}
496		sysoPath, err := shouldBuildSyso(cwd)
497		if err != nil {
498			log.Printf("Warning: Windows binaries will not have file information encoded: %v", err)
499		}
500		defer shouldCleanupSyso(sysoPath)
501	}
502
503	args := []string{"build", "-v"}
504	args = appendParameters(args, tags, target.buildPkgs...)
505	runPrint(goCmd, args...)
506}
507
508func setBuildEnvVars() {
509	os.Setenv("GOOS", goos)
510	os.Setenv("GOARCH", goarch)
511	os.Setenv("CC", cc)
512	if os.Getenv("CGO_ENABLED") == "" {
513		switch goos {
514		case "darwin", "solaris":
515		default:
516			os.Setenv("CGO_ENABLED", "0")
517		}
518	}
519}
520
521func appendParameters(args []string, tags []string, pkgs ...string) []string {
522	if pkgdir != "" {
523		args = append(args, "-pkgdir", pkgdir)
524	}
525	if len(tags) > 0 {
526		args = append(args, "-tags", strings.Join(tags, " "))
527	}
528	if installSuffix != "" {
529		args = append(args, "-installsuffix", installSuffix)
530	}
531	if race {
532		args = append(args, "-race")
533	}
534
535	if !debugBinary {
536		// Regular binaries get version tagged and skip some debug symbols
537		args = append(args, "-trimpath", "-ldflags", ldflags(tags))
538	} else {
539		// -gcflags to disable optimizations and inlining. Skip -ldflags
540		// because `Could not launch program: decoding dwarf section info at
541		// offset 0x0: too short` on 'dlv exec ...' see
542		// https://github.com/go-delve/delve/issues/79
543		args = append(args, "-gcflags", "all=-N -l")
544	}
545
546	return append(args, pkgs...)
547}
548
549func buildTar(target target, tags []string) {
550	name := archiveName(target)
551	filename := name + ".tar.gz"
552
553	for _, tag := range tags {
554		if tag == "noupgrade" {
555			name += "-noupgrade"
556			break
557		}
558	}
559
560	build(target, tags)
561	codesign(target)
562
563	for i := range target.archiveFiles {
564		target.archiveFiles[i].src = strings.Replace(target.archiveFiles[i].src, "{{binary}}", target.BinaryName(), 1)
565		target.archiveFiles[i].dst = strings.Replace(target.archiveFiles[i].dst, "{{binary}}", target.BinaryName(), 1)
566		target.archiveFiles[i].dst = name + "/" + target.archiveFiles[i].dst
567	}
568
569	tarGz(filename, target.archiveFiles)
570	fmt.Println(filename)
571}
572
573func buildZip(target target, tags []string) {
574	name := archiveName(target)
575	filename := name + ".zip"
576
577	for _, tag := range tags {
578		if tag == "noupgrade" {
579			name += "-noupgrade"
580			break
581		}
582	}
583
584	build(target, tags)
585	codesign(target)
586
587	for i := range target.archiveFiles {
588		target.archiveFiles[i].src = strings.Replace(target.archiveFiles[i].src, "{{binary}}", target.BinaryName(), 1)
589		target.archiveFiles[i].dst = strings.Replace(target.archiveFiles[i].dst, "{{binary}}", target.BinaryName(), 1)
590		target.archiveFiles[i].dst = name + "/" + target.archiveFiles[i].dst
591	}
592
593	zipFile(filename, target.archiveFiles)
594	fmt.Println(filename)
595}
596
597func buildDeb(target target) {
598	os.RemoveAll("deb")
599
600	// "goarch" here is set to whatever the Debian packages expect. We correct
601	// it to what we actually know how to build and keep the Debian variant
602	// name in "debarch".
603	debarch := goarch
604	switch goarch {
605	case "i386":
606		goarch = "386"
607	case "armel", "armhf":
608		goarch = "arm"
609	}
610
611	build(target, []string{"noupgrade"})
612
613	for i := range target.installationFiles {
614		target.installationFiles[i].src = strings.Replace(target.installationFiles[i].src, "{{binary}}", target.BinaryName(), 1)
615		target.installationFiles[i].dst = strings.Replace(target.installationFiles[i].dst, "{{binary}}", target.BinaryName(), 1)
616	}
617
618	for _, af := range target.installationFiles {
619		if err := copyFile(af.src, af.dst, af.perm); err != nil {
620			log.Fatal(err)
621		}
622	}
623
624	maintainer := "Syncthing Release Management <release@syncthing.net>"
625	debver := version
626	if strings.HasPrefix(debver, "v") {
627		debver = debver[1:]
628		// Debian interprets dashes as separator between main version and
629		// Debian package version, and thus thinks 0.14.26-rc.1 is better
630		// than just 0.14.26. This rectifies that.
631		debver = strings.Replace(debver, "-", "~", -1)
632	}
633	args := []string{
634		"-t", "deb",
635		"-s", "dir",
636		"-C", "deb",
637		"-n", target.debname,
638		"-v", debver,
639		"-a", debarch,
640		"-m", maintainer,
641		"--vendor", maintainer,
642		"--description", target.description,
643		"--url", "https://syncthing.net/",
644		"--license", "MPL-2",
645	}
646	for _, dep := range target.debdeps {
647		args = append(args, "-d", dep)
648	}
649	if target.systemdService != "" {
650		debpost, err := createPostInstScript(target)
651		defer os.Remove(debpost)
652		if err != nil {
653			log.Fatal(err)
654		}
655		args = append(args, "--after-upgrade", debpost)
656	}
657	if target.debpre != "" {
658		args = append(args, "--before-install", target.debpre)
659	}
660	runPrint("fpm", args...)
661}
662
663func createPostInstScript(target target) (string, error) {
664	scriptname := filepath.Join("script", "deb-post-inst.template")
665	t, err := template.ParseFiles(scriptname)
666	if err != nil {
667		return "", err
668	}
669	scriptname = strings.TrimSuffix(scriptname, ".template")
670	w, err := os.Create(scriptname)
671	if err != nil {
672		return "", err
673	}
674	defer w.Close()
675	if err = t.Execute(w, struct {
676		Service, Command string
677	}{
678		target.systemdService, target.binaryName,
679	}); err != nil {
680		return "", err
681	}
682	return scriptname, nil
683}
684
685func shouldBuildSyso(dir string) (string, error) {
686	type M map[string]interface{}
687	version := getVersion()
688	version = strings.TrimPrefix(version, "v")
689	major, minor, patch := semanticVersion()
690	bs, err := json.Marshal(M{
691		"FixedFileInfo": M{
692			"FileVersion": M{
693				"Major": major,
694				"Minor": minor,
695				"Patch": patch,
696			},
697			"ProductVersion": M{
698				"Major": major,
699				"Minor": minor,
700				"Patch": patch,
701			},
702		},
703		"StringFileInfo": M{
704			"CompanyName":      "The Syncthing Authors",
705			"FileDescription":  "Syncthing - Open Source Continuous File Synchronization",
706			"FileVersion":      version,
707			"InternalName":     "syncthing",
708			"LegalCopyright":   "The Syncthing Authors",
709			"OriginalFilename": "syncthing",
710			"ProductName":      "Syncthing",
711			"ProductVersion":   version,
712		},
713		"IconPath": "assets/logo.ico",
714	})
715	if err != nil {
716		return "", err
717	}
718
719	jsonPath := filepath.Join(dir, "versioninfo.json")
720	err = ioutil.WriteFile(jsonPath, bs, 0644)
721	if err != nil {
722		return "", errors.New("failed to create " + jsonPath + ": " + err.Error())
723	}
724
725	defer func() {
726		if err := os.Remove(jsonPath); err != nil {
727			log.Printf("Warning: unable to remove generated %s: %v. Please remove it manually.", jsonPath, err)
728		}
729	}()
730
731	sysoPath := filepath.Join(dir, "cmd", "syncthing", "resource.syso")
732
733	if _, err := runError("goversioninfo", "-o", sysoPath); err != nil {
734		return "", errors.New("failed to create " + sysoPath + ": " + err.Error())
735	}
736
737	return sysoPath, nil
738}
739
740func shouldCleanupSyso(sysoFilePath string) {
741	if sysoFilePath == "" {
742		return
743	}
744	if err := os.Remove(sysoFilePath); err != nil {
745		log.Printf("Warning: unable to remove generated %s: %v. Please remove it manually.", sysoFilePath, err)
746	}
747}
748
749// copyFile copies a file from src to dst, ensuring the containing directory
750// exists. The permission bits are copied as well. If dst already exists and
751// the contents are identical to src the modification time is not updated.
752func copyFile(src, dst string, perm os.FileMode) error {
753	in, err := ioutil.ReadFile(src)
754	if err != nil {
755		return err
756	}
757
758	out, err := ioutil.ReadFile(dst)
759	if err != nil {
760		// The destination probably doesn't exist, we should create
761		// it.
762		goto copy
763	}
764
765	if bytes.Equal(in, out) {
766		// The permission bits may have changed without the contents
767		// changing so we always mirror them.
768		os.Chmod(dst, perm)
769		return nil
770	}
771
772copy:
773	os.MkdirAll(filepath.Dir(dst), 0777)
774	if err := ioutil.WriteFile(dst, in, perm); err != nil {
775		return err
776	}
777
778	return nil
779}
780
781func listFiles(dir string) []string {
782	var res []string
783	filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
784		if err != nil {
785			return err
786		}
787
788		if fi.Mode().IsRegular() {
789			res = append(res, path)
790		}
791		return nil
792	})
793	return res
794}
795
796func rebuildAssets() {
797	os.Setenv("SOURCE_DATE_EPOCH", fmt.Sprint(buildStamp()))
798	runPrint(goCmd, "generate", "github.com/syncthing/syncthing/lib/api/auto", "github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto")
799}
800
801func lazyRebuildAssets() {
802	shouldRebuild := shouldRebuildAssets("lib/api/auto/gui.files.go", "gui") ||
803		shouldRebuildAssets("cmd/strelaypoolsrv/auto/gui.files.go", "cmd/strelaypoolsrv/gui")
804
805	if withNextGenGUI {
806		shouldRebuild = buildNextGenGUI() || shouldRebuild
807	}
808
809	if shouldRebuild {
810		rebuildAssets()
811	}
812}
813
814func buildNextGenGUI() bool {
815	// Check if we need to run the npm process, and if so also set the flag
816	// to rebuild Go assets afterwards. The index.html is regenerated every
817	// time by the build process. This assumes the new GUI ends up in
818	// next-gen-gui/dist/next-gen-gui.
819
820	if !shouldRebuildAssets("gui/next-gen-gui/index.html", "next-gen-gui") {
821		// The GUI is up to date.
822		return false
823	}
824
825	runPrintInDir("next-gen-gui", "npm", "install")
826	runPrintInDir("next-gen-gui", "npm", "run", "build", "--", "--prod", "--subresource-integrity")
827
828	rmr("gui/tech-ui")
829
830	for _, src := range listFiles("next-gen-gui/dist") {
831		rel, _ := filepath.Rel("next-gen-gui/dist", src)
832		dst := filepath.Join("gui", rel)
833		if err := copyFile(src, dst, 0644); err != nil {
834			fmt.Println("copy:", err)
835			os.Exit(1)
836		}
837	}
838
839	return true
840}
841
842func shouldRebuildAssets(target, srcdir string) bool {
843	info, err := os.Stat(target)
844	if err != nil {
845		// If the file doesn't exist, we must rebuild it
846		return true
847	}
848
849	// Check if any of the files in gui/ are newer than the asset file. If
850	// so we should rebuild it.
851	currentBuild := info.ModTime()
852	assetsAreNewer := false
853	stop := errors.New("no need to iterate further")
854	filepath.Walk(srcdir, func(path string, info os.FileInfo, err error) error {
855		if err != nil {
856			return err
857		}
858		if info.ModTime().After(currentBuild) {
859			assetsAreNewer = true
860			return stop
861		}
862		return nil
863	})
864
865	return assetsAreNewer
866}
867
868func proto() {
869	pv := protobufVersion()
870	repo := "https://github.com/gogo/protobuf.git"
871	path := filepath.Join("repos", "protobuf")
872
873	runPrint(goCmd, "get", fmt.Sprintf("github.com/gogo/protobuf/protoc-gen-gogofast@%v", pv))
874	os.MkdirAll("repos", 0755)
875
876	if _, err := os.Stat(path); err != nil {
877		runPrint("git", "clone", repo, path)
878	} else {
879		runPrintInDir(path, "git", "fetch")
880	}
881	runPrintInDir(path, "git", "checkout", pv)
882
883	runPrint(goCmd, "generate", "github.com/syncthing/syncthing/cmd/stdiscosrv")
884	runPrint(goCmd, "generate", "proto/generate.go")
885}
886
887func testmocks() {
888	args := []string{
889		"generate",
890		"github.com/syncthing/syncthing/lib/config",
891		"github.com/syncthing/syncthing/lib/connections",
892		"github.com/syncthing/syncthing/lib/discover",
893		"github.com/syncthing/syncthing/lib/events",
894		"github.com/syncthing/syncthing/lib/logger",
895		"github.com/syncthing/syncthing/lib/model",
896		"github.com/syncthing/syncthing/lib/protocol",
897	}
898	runPrint(goCmd, args...)
899}
900
901func translate() {
902	os.Chdir("gui/default/assets/lang")
903	runPipe("lang-en-new.json", goCmd, "run", "../../../../script/translate.go", "lang-en.json", "../../../")
904	os.Remove("lang-en.json")
905	err := os.Rename("lang-en-new.json", "lang-en.json")
906	if err != nil {
907		log.Fatal(err)
908	}
909	os.Chdir("../../../..")
910}
911
912func transifex() {
913	os.Chdir("gui/default/assets/lang")
914	runPrint(goCmd, "run", "../../../../script/transifexdl.go")
915}
916
917func ldflags(tags []string) string {
918	b := new(strings.Builder)
919	b.WriteString("-w")
920	fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Version=%s", version)
921	fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Stamp=%d", buildStamp())
922	fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.User=%s", buildUser())
923	fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Host=%s", buildHost())
924	fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Tags=%s", strings.Join(tags, ","))
925	if v := os.Getenv("EXTRA_LDFLAGS"); v != "" {
926		fmt.Fprintf(b, " %s", v)
927	}
928	return b.String()
929}
930
931func rmr(paths ...string) {
932	for _, path := range paths {
933		if debug {
934			log.Println("rm -r", path)
935		}
936		os.RemoveAll(path)
937	}
938}
939
940func getReleaseVersion() (string, error) {
941	bs, err := ioutil.ReadFile("RELEASE")
942	if err != nil {
943		return "", err
944	}
945	return string(bytes.TrimSpace(bs)), nil
946}
947
948func getGitVersion() (string, error) {
949	// The current version as Git sees it
950	bs, err := runError("git", "describe", "--always", "--dirty", "--abbrev=8")
951	if err != nil {
952		return "", err
953	}
954	vcur := string(bs)
955
956	// The closest current tag name
957	bs, err = runError("git", "describe", "--always", "--abbrev=0")
958	if err != nil {
959		return "", err
960	}
961	v0 := string(bs)
962
963	// To be more semantic-versionish and ensure proper ordering in our
964	// upgrade process, we make sure there's only one hypen in the version.
965
966	versionRe := regexp.MustCompile(`-([0-9]{1,3}-g[0-9a-f]{5,10}(-dirty)?)`)
967	if m := versionRe.FindStringSubmatch(vcur); len(m) > 0 {
968		suffix := strings.ReplaceAll(m[1], "-", ".")
969
970		if strings.Contains(v0, "-") {
971			// We're based of a tag with a prerelease string. We can just
972			// add our dev stuff directly.
973			return fmt.Sprintf("%s.dev.%s", v0, suffix), nil
974		}
975
976		// We're based on a release version. We need to bump the patch
977		// version and then add a -dev prerelease string.
978		next := nextPatchVersion(v0)
979		return fmt.Sprintf("%s-dev.%s", next, suffix), nil
980	}
981	return vcur, nil
982}
983
984func getVersion() string {
985	// First try for a RELEASE file,
986	if ver, err := getReleaseVersion(); err == nil {
987		return ver
988	}
989	// ... then see if we have a Git tag.
990	if ver, err := getGitVersion(); err == nil {
991		if strings.Contains(ver, "-") {
992			// The version already contains a hash and stuff. See if we can
993			// find a current branch name to tack onto it as well.
994			return ver + getBranchSuffix()
995		}
996		return ver
997	}
998	// This seems to be a dev build.
999	return "unknown-dev"
1000}
1001
1002func semanticVersion() (major, minor, patch int) {
1003	r := regexp.MustCompile(`v(\d+)\.(\d+).(\d+)`)
1004	matches := r.FindStringSubmatch(getVersion())
1005	if len(matches) != 4 {
1006		return 0, 0, 0
1007	}
1008
1009	var ints [3]int
1010	for i, s := range matches[1:] {
1011		ints[i], _ = strconv.Atoi(s)
1012	}
1013	return ints[0], ints[1], ints[2]
1014}
1015
1016func getBranchSuffix() string {
1017	bs, err := runError("git", "branch", "-a", "--contains")
1018	if err != nil {
1019		return ""
1020	}
1021
1022	branches := strings.Split(string(bs), "\n")
1023	if len(branches) == 0 {
1024		return ""
1025	}
1026
1027	branch := ""
1028	for i, candidate := range branches {
1029		if strings.HasPrefix(candidate, "*") {
1030			// This is the current branch. Select it!
1031			branch = strings.TrimLeft(candidate, " \t*")
1032			break
1033		} else if i == 0 {
1034			// Otherwise the first branch in the list will do.
1035			branch = strings.TrimSpace(branch)
1036		}
1037	}
1038
1039	if branch == "" {
1040		return ""
1041	}
1042
1043	// The branch name may be on the form "remotes/origin/foo" from which we
1044	// just want "foo".
1045	parts := strings.Split(branch, "/")
1046	if len(parts) == 0 || len(parts[len(parts)-1]) == 0 {
1047		return ""
1048	}
1049
1050	branch = parts[len(parts)-1]
1051	switch branch {
1052	case "master", "release", "main":
1053		// these are not special
1054		return ""
1055	}
1056
1057	validBranchRe := regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`)
1058	if !validBranchRe.MatchString(branch) {
1059		// There's some odd stuff in the branch name. Better skip it.
1060		return ""
1061	}
1062
1063	return "-" + branch
1064}
1065
1066func buildStamp() int64 {
1067	// If SOURCE_DATE_EPOCH is set, use that.
1068	if s, _ := strconv.ParseInt(os.Getenv("SOURCE_DATE_EPOCH"), 10, 64); s > 0 {
1069		return s
1070	}
1071
1072	// Try to get the timestamp of the latest commit.
1073	bs, err := runError("git", "show", "-s", "--format=%ct")
1074	if err != nil {
1075		// Fall back to "now".
1076		return time.Now().Unix()
1077	}
1078
1079	s, _ := strconv.ParseInt(string(bs), 10, 64)
1080	return s
1081}
1082
1083func buildUser() string {
1084	if v := os.Getenv("BUILD_USER"); v != "" {
1085		return v
1086	}
1087
1088	u, err := user.Current()
1089	if err != nil {
1090		return "unknown-user"
1091	}
1092	return strings.Replace(u.Username, " ", "-", -1)
1093}
1094
1095func buildHost() string {
1096	if v := os.Getenv("BUILD_HOST"); v != "" {
1097		return v
1098	}
1099
1100	h, err := os.Hostname()
1101	if err != nil {
1102		return "unknown-host"
1103	}
1104	return h
1105}
1106
1107func buildArch() string {
1108	os := goos
1109	if os == "darwin" {
1110		os = "macos"
1111	}
1112	return fmt.Sprintf("%s-%s", os, goarch)
1113}
1114
1115func archiveName(target target) string {
1116	return fmt.Sprintf("%s-%s-%s", target.name, buildArch(), version)
1117}
1118
1119func runError(cmd string, args ...string) ([]byte, error) {
1120	if debug {
1121		t0 := time.Now()
1122		log.Println("runError:", cmd, strings.Join(args, " "))
1123		defer func() {
1124			log.Println("... in", time.Since(t0))
1125		}()
1126	}
1127	ecmd := exec.Command(cmd, args...)
1128	bs, err := ecmd.CombinedOutput()
1129	return bytes.TrimSpace(bs), err
1130}
1131
1132func runPrint(cmd string, args ...string) {
1133	runPrintInDir(".", cmd, args...)
1134}
1135
1136func runPrintInDir(dir string, cmd string, args ...string) {
1137	if debug {
1138		t0 := time.Now()
1139		log.Println("runPrint:", cmd, strings.Join(args, " "))
1140		defer func() {
1141			log.Println("... in", time.Since(t0))
1142		}()
1143	}
1144	ecmd := exec.Command(cmd, args...)
1145	ecmd.Stdout = os.Stdout
1146	ecmd.Stderr = os.Stderr
1147	ecmd.Dir = dir
1148	err := ecmd.Run()
1149	if err != nil {
1150		log.Fatal(err)
1151	}
1152}
1153
1154func runPipe(file, cmd string, args ...string) {
1155	if debug {
1156		t0 := time.Now()
1157		log.Println("runPipe:", cmd, strings.Join(args, " "))
1158		defer func() {
1159			log.Println("... in", time.Since(t0))
1160		}()
1161	}
1162	fd, err := os.Create(file)
1163	if err != nil {
1164		log.Fatal(err)
1165	}
1166	ecmd := exec.Command(cmd, args...)
1167	ecmd.Stdout = fd
1168	ecmd.Stderr = os.Stderr
1169	err = ecmd.Run()
1170	if err != nil {
1171		log.Fatal(err)
1172	}
1173	fd.Close()
1174}
1175
1176func tarGz(out string, files []archiveFile) {
1177	fd, err := os.Create(out)
1178	if err != nil {
1179		log.Fatal(err)
1180	}
1181
1182	gw, err := gzip.NewWriterLevel(fd, gzip.BestCompression)
1183	if err != nil {
1184		log.Fatal(err)
1185	}
1186	tw := tar.NewWriter(gw)
1187
1188	for _, f := range files {
1189		sf, err := os.Open(f.src)
1190		if err != nil {
1191			log.Fatal(err)
1192		}
1193
1194		info, err := sf.Stat()
1195		if err != nil {
1196			log.Fatal(err)
1197		}
1198		h := &tar.Header{
1199			Name:    f.dst,
1200			Size:    info.Size(),
1201			Mode:    int64(info.Mode()),
1202			ModTime: info.ModTime(),
1203		}
1204
1205		err = tw.WriteHeader(h)
1206		if err != nil {
1207			log.Fatal(err)
1208		}
1209		_, err = io.Copy(tw, sf)
1210		if err != nil {
1211			log.Fatal(err)
1212		}
1213		sf.Close()
1214	}
1215
1216	err = tw.Close()
1217	if err != nil {
1218		log.Fatal(err)
1219	}
1220	err = gw.Close()
1221	if err != nil {
1222		log.Fatal(err)
1223	}
1224	err = fd.Close()
1225	if err != nil {
1226		log.Fatal(err)
1227	}
1228}
1229
1230func zipFile(out string, files []archiveFile) {
1231	fd, err := os.Create(out)
1232	if err != nil {
1233		log.Fatal(err)
1234	}
1235
1236	zw := zip.NewWriter(fd)
1237
1238	var fw *flate.Writer
1239
1240	// Register the deflator.
1241	zw.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
1242		var err error
1243		if fw == nil {
1244			// Creating a flate compressor for every file is
1245			// expensive, create one and reuse it.
1246			fw, err = flate.NewWriter(out, flate.BestCompression)
1247		} else {
1248			fw.Reset(out)
1249		}
1250		return fw, err
1251	})
1252
1253	for _, f := range files {
1254		sf, err := os.Open(f.src)
1255		if err != nil {
1256			log.Fatal(err)
1257		}
1258
1259		info, err := sf.Stat()
1260		if err != nil {
1261			log.Fatal(err)
1262		}
1263
1264		fh, err := zip.FileInfoHeader(info)
1265		if err != nil {
1266			log.Fatal(err)
1267		}
1268		fh.Name = filepath.ToSlash(f.dst)
1269		fh.Method = zip.Deflate
1270
1271		if strings.HasSuffix(f.dst, ".txt") {
1272			// Text file. Read it and convert line endings.
1273			bs, err := ioutil.ReadAll(sf)
1274			if err != nil {
1275				log.Fatal(err)
1276			}
1277			bs = bytes.Replace(bs, []byte{'\n'}, []byte{'\n', '\r'}, -1)
1278			fh.UncompressedSize = uint32(len(bs))
1279			fh.UncompressedSize64 = uint64(len(bs))
1280
1281			of, err := zw.CreateHeader(fh)
1282			if err != nil {
1283				log.Fatal(err)
1284			}
1285			of.Write(bs)
1286		} else {
1287			// Binary file. Copy verbatim.
1288			of, err := zw.CreateHeader(fh)
1289			if err != nil {
1290				log.Fatal(err)
1291			}
1292			_, err = io.Copy(of, sf)
1293			if err != nil {
1294				log.Fatal(err)
1295			}
1296		}
1297	}
1298
1299	err = zw.Close()
1300	if err != nil {
1301		log.Fatal(err)
1302	}
1303	err = fd.Close()
1304	if err != nil {
1305		log.Fatal(err)
1306	}
1307}
1308
1309func codesign(target target) {
1310	switch goos {
1311	case "windows":
1312		windowsCodesign(target.BinaryName())
1313	case "darwin":
1314		macosCodesign(target.BinaryName())
1315	}
1316}
1317
1318func macosCodesign(file string) {
1319	if pass := os.Getenv("CODESIGN_KEYCHAIN_PASS"); pass != "" {
1320		bs, err := runError("security", "unlock-keychain", "-p", pass)
1321		if err != nil {
1322			log.Println("Codesign: unlocking keychain failed:", string(bs))
1323			return
1324		}
1325	}
1326
1327	if id := os.Getenv("CODESIGN_IDENTITY"); id != "" {
1328		bs, err := runError("codesign", "--options=runtime", "-s", id, file)
1329		if err != nil {
1330			log.Println("Codesign: signing failed:", string(bs))
1331			return
1332		}
1333		log.Println("Codesign: successfully signed", file)
1334	}
1335}
1336
1337func windowsCodesign(file string) {
1338	st := "signtool.exe"
1339
1340	if path := os.Getenv("CODESIGN_SIGNTOOL"); path != "" {
1341		st = path
1342	}
1343
1344	for i, algo := range []string{"sha1", "sha256"} {
1345		args := []string{"sign", "/fd", algo}
1346		if f := os.Getenv("CODESIGN_CERTIFICATE_FILE"); f != "" {
1347			args = append(args, "/f", f)
1348		}
1349		if p := os.Getenv("CODESIGN_CERTIFICATE_PASSWORD"); p != "" {
1350			args = append(args, "/p", p)
1351		}
1352		if tr := os.Getenv("CODESIGN_TIMESTAMP_SERVER"); tr != "" {
1353			switch algo {
1354			case "sha256":
1355				args = append(args, "/tr", tr, "/td", algo)
1356			default:
1357				args = append(args, "/t", tr)
1358			}
1359		}
1360		if i > 0 {
1361			args = append(args, "/as")
1362		}
1363		args = append(args, file)
1364
1365		bs, err := runError(st, args...)
1366		if err != nil {
1367			log.Println("Codesign: signing failed:", string(bs))
1368			return
1369		}
1370		log.Println("Codesign: successfully signed", file, "using", algo)
1371	}
1372}
1373
1374func metalint() {
1375	lazyRebuildAssets()
1376	runPrint(goCmd, "test", "-run", "Metalint", "./meta")
1377}
1378
1379func metalintShort() {
1380	lazyRebuildAssets()
1381	runPrint(goCmd, "test", "-short", "-run", "Metalint", "./meta")
1382}
1383
1384func (t target) BinaryName() string {
1385	if goos == "windows" {
1386		return t.binaryName + ".exe"
1387	}
1388	return t.binaryName
1389}
1390
1391func protobufVersion() string {
1392	bs, err := runError(goCmd, "list", "-f", "{{.Version}}", "-m", "github.com/gogo/protobuf")
1393	if err != nil {
1394		log.Fatal("Getting protobuf version:", err)
1395	}
1396	return string(bs)
1397}
1398
1399func currentAndLatestVersions(n int) ([]string, error) {
1400	bs, err := runError("git", "tag", "--sort", "taggerdate")
1401	if err != nil {
1402		return nil, err
1403	}
1404
1405	lines := strings.Split(string(bs), "\n")
1406	reverseStrings(lines)
1407
1408	// The one at the head is the latest version. We always keep that one.
1409	// Then we filter out remaining ones with dashes (pre-releases etc).
1410
1411	latest := lines[:1]
1412	nonPres := filterStrings(lines[1:], func(s string) bool { return !strings.Contains(s, "-") })
1413	vers := append(latest, nonPres...)
1414	return vers[:n], nil
1415}
1416
1417func reverseStrings(ss []string) {
1418	for i := 0; i < len(ss)/2; i++ {
1419		ss[i], ss[len(ss)-1-i] = ss[len(ss)-1-i], ss[i]
1420	}
1421}
1422
1423func filterStrings(ss []string, op func(string) bool) []string {
1424	n := ss[:0]
1425	for _, s := range ss {
1426		if op(s) {
1427			n = append(n, s)
1428		}
1429	}
1430	return n
1431}
1432
1433func tagMessage(tag string) (string, error) {
1434	hash, err := runError("git", "rev-parse", tag)
1435	if err != nil {
1436		return "", err
1437	}
1438	obj, err := runError("git", "cat-file", "-p", string(hash))
1439	if err != nil {
1440		return "", err
1441	}
1442	return trimTagMessage(string(obj), tag), nil
1443}
1444
1445func trimTagMessage(msg, tag string) string {
1446	firstBlank := strings.Index(msg, "\n\n")
1447	if firstBlank > 0 {
1448		msg = msg[firstBlank+2:]
1449	}
1450	msg = strings.TrimPrefix(msg, tag)
1451	beginSig := strings.Index(msg, "-----BEGIN PGP")
1452	if beginSig > 0 {
1453		msg = msg[:beginSig]
1454	}
1455	return strings.TrimSpace(msg)
1456}
1457
1458func nextPatchVersion(ver string) string {
1459	parts := strings.SplitN(ver, "-", 2)
1460	digits := strings.Split(parts[0], ".")
1461	n, _ := strconv.Atoi(digits[len(digits)-1])
1462	digits[len(digits)-1] = strconv.Itoa(n + 1)
1463	return strings.Join(digits, ".")
1464}
1465