// Copyright 2012 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This is a tool for packaging binary releases. // It supports FreeBSD, Linux, NetBSD, OpenBSD, OS X, and Windows. package main import ( "archive/tar" "archive/zip" "bufio" "bytes" "compress/gzip" "crypto/sha1" "encoding/json" "flag" "fmt" "io" "io/ioutil" "log" "net/http" "net/url" "os" "os/exec" "path" "path/filepath" "regexp" "runtime" "strings" "code.google.com/p/goauth2/oauth" "code.google.com/p/google-api-go-client/storage/v1beta2" ) var ( tag = flag.String("tag", "release", "mercurial tag to check out") toolTag = flag.String("tool", defaultToolTag, "go.tools tag to check out") tourTag = flag.String("tour", defaultTourTag, "go-tour tag to check out") repo = flag.String("repo", "https://code.google.com/p/go", "repo URL") verbose = flag.Bool("v", false, "verbose output") upload = flag.Bool("upload", false, "upload resulting files to Google Code") addLabel = flag.String("label", "", "additional label to apply to file when uploading") includeRace = flag.Bool("race", true, "build race detector packages") versionOverride = flag.String("version", "", "override version name") staticToolchain = flag.Bool("static", true, "try to build statically linked toolchain (only supported on ELF targets)") tokenCache = flag.String("token", defaultCacheFile, "Authentication token cache file") storageBucket = flag.String("bucket", "golang", "Cloud Storage Bucket") uploadURL = flag.String("upload_url", defaultUploadURL, "Upload URL") defaultCacheFile = filepath.Join(os.Getenv("HOME"), ".makerelease-request-token") defaultUploadURL = "http://golang.org/dl/upload" ) const ( blogPath = "code.google.com/p/go.blog" toolPath = "code.google.com/p/go.tools" tourPath = "code.google.com/p/go-tour" defaultToolTag = "release-branch.go1.2" defaultTourTag = "release-branch.go1.2" ) // Import paths for tool commands. // These must be the command that cmd/go knows to install to $GOROOT/bin // or $GOROOT/pkg/tool. var toolPaths = []string{ "code.google.com/p/go.tools/cmd/cover", "code.google.com/p/go.tools/cmd/godoc", "code.google.com/p/go.tools/cmd/vet", } var preBuildCleanFiles = []string{ "lib/codereview", "misc/dashboard/godashboard", "src/cmd/cov", "src/cmd/prof", "src/pkg/exp", "src/pkg/old", } var cleanFiles = []string{ ".hg", ".hgtags", ".hgignore", "VERSION.cache", } var sourceCleanFiles = []string{ "bin", "pkg", } var tourPackages = []string{ "pic", "tree", "wc", } var tourContent = []string{ "content", "solutions", "static", "template", } var blogContent = []string{ "content", "template", } // The os-arches that support the race toolchain. var raceAvailable = []string{ "darwin-amd64", "linux-amd64", "windows-amd64", } // The OSes that support building statically linked toolchain // Only ELF platforms are supported. var staticLinkAvailable = []string{ "linux", "freebsd", "openbsd", "netbsd", } var fileRe = regexp.MustCompile(`^(go[a-z0-9-.]+)\.(src|([a-z0-9]+)-([a-z0-9]+)(?:-([a-z0-9.]+))?)\.(tar\.gz|zip|pkg|msi)$`) // OAuth2-authenticated HTTP client used to make calls to Cloud Storage. var oauthClient *http.Client // Builder key as specified in ~/.gobuildkey var builderKey string func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "usage: %s [flags] targets...\n", os.Args[0]) flag.PrintDefaults() os.Exit(2) } flag.Parse() if flag.NArg() == 0 { flag.Usage() } if runtime.GOOS == "windows" { checkWindowsDeps() } if *upload { if err := readCredentials(); err != nil { log.Fatalln("readCredentials:", err) } if err := setupOAuthClient(); err != nil { log.Fatalln("setupOAuthClient:", err) } } for _, targ := range flag.Args() { var b Build if m := fileRe.FindStringSubmatch(targ); m != nil { // targ is a file name; upload it to googlecode. version := m[1] if m[2] == "src" { b.Source = true } else { b.OS = m[3] b.Arch = m[4] b.Label = m[5] } if !*upload { log.Printf("%s: -upload=false, skipping", targ) continue } if err := b.Upload(version, targ); err != nil { log.Printf("uploading %s: %v", targ, err) } continue } if targ == "source" { b.Source = true } else { p := strings.SplitN(targ, "-", 3) if len(p) < 2 { log.Println("Ignoring unrecognized target:", targ) continue } b.OS = p[0] b.Arch = p[1] if len(p) >= 3 { b.Label = p[2] } if *includeRace { for _, t := range raceAvailable { if t == targ || strings.HasPrefix(targ, t+"-") { b.Race = true } } } if *staticToolchain { for _, os := range staticLinkAvailable { if b.OS == os { b.static = true } } } } if err := b.Do(); err != nil { log.Printf("%s: %v", targ, err) } } } type Build struct { Source bool // if true, OS and Arch must be empty Race bool // build race toolchain OS string Arch string Label string root string gopath string static bool // if true, build statically linked toolchain } func (b *Build) Do() error { work, err := ioutil.TempDir("", "makerelease") if err != nil { return err } defer os.RemoveAll(work) b.root = filepath.Join(work, "go") b.gopath = work // Clone Go distribution and update to tag. _, err = b.hgCmd(work, "clone", *repo, b.root) if err != nil { return err } _, err = b.hgCmd(b.root, "update", *tag) if err != nil { return err } // Remove exp and old packages. if err := b.clean(preBuildCleanFiles); err != nil { return err } src := filepath.Join(b.root, "src") if b.Source { if runtime.GOOS == "windows" { log.Print("Warning: running make.bash on Windows; source builds are intended to be run on a Unix machine") } // Build dist tool only. _, err = b.run(src, "bash", "make.bash", "--dist-tool") } else { // Build. if b.OS == "windows" { _, err = b.run(src, "cmd", "/C", "make.bat") } else { _, err = b.run(src, "bash", "make.bash") } if b.Race { if err != nil { return err } goCmd := filepath.Join(b.root, "bin", "go") if b.OS == "windows" { goCmd += ".exe" } _, err = b.run(src, goCmd, "install", "-race", "std") if err != nil { return err } // Re-install std without -race, so that we're not left // with a slower, race-enabled cmd/go, etc. _, err = b.run(src, goCmd, "install", "-a", "std") // Re-building go command leaves old versions of go.exe as go.exe~ on windows. // See (*builder).copyFile in $GOROOT/src/cmd/go/build.go for details. // Remove it manually. if b.OS == "windows" { os.Remove(goCmd + "~") } } if err != nil { return err } err = b.extras() } if err != nil { return err } // Get version strings. var ( version string // "weekly.2012-03-04" fullVersion []byte // "weekly.2012-03-04 9353aa1efdf3" ) pat := filepath.Join(b.root, "pkg/tool/*/dist*") // trailing * for .exe m, err := filepath.Glob(pat) if err != nil { return err } if len(m) == 0 { return fmt.Errorf("couldn't find dist in %q", pat) } fullVersion, err = b.run("", m[0], "version") if err != nil { return err } fullVersion = bytes.TrimSpace(fullVersion) v := bytes.SplitN(fullVersion, []byte(" "), 2) version = string(v[0]) if *versionOverride != "" { version = *versionOverride } // Write VERSION file. err = ioutil.WriteFile(filepath.Join(b.root, "VERSION"), fullVersion, 0644) if err != nil { return err } // Clean goroot. if err := b.clean(cleanFiles); err != nil { return err } if b.Source { if err := b.clean(sourceCleanFiles); err != nil { return err } } // Create packages. base := fmt.Sprintf("%s.%s-%s", version, b.OS, b.Arch) if b.Label != "" { base += "-" + b.Label } if !strings.HasPrefix(base, "go") { base = "go." + base } var targs []string switch b.OS { case "linux", "freebsd", "netbsd", "": // build tarball targ := base if b.Source { targ = fmt.Sprintf("%s.src", version) if !strings.HasPrefix(targ, "go") { targ = "go." + targ } } targ += ".tar.gz" err = makeTar(targ, work) targs = append(targs, targ) case "darwin": // build tarball targ := base + ".tar.gz" err = makeTar(targ, work) targs = append(targs, targ) makerelease := filepath.Join(runtime.GOROOT(), "misc/makerelease") // build pkg // arrange work so it's laid out as the dest filesystem etc := filepath.Join(makerelease, "darwin/etc") _, err = b.run(work, "cp", "-r", etc, ".") if err != nil { return err } localDir := filepath.Join(work, "usr/local") err = os.MkdirAll(localDir, 0755) if err != nil { return err } _, err = b.run(work, "mv", "go", localDir) if err != nil { return err } // build package pkgdest, err := ioutil.TempDir("", "pkgdest") if err != nil { return err } defer os.RemoveAll(pkgdest) _, err = b.run("", "pkgbuild", "--identifier", "com.googlecode.go", "--version", version, "--scripts", filepath.Join(makerelease, "darwin/scripts"), "--root", work, filepath.Join(pkgdest, "com.googlecode.go.pkg")) if err != nil { return err } targ = base + ".pkg" _, err = b.run("", "productbuild", "--distribution", filepath.Join(makerelease, "darwin/Distribution"), "--resources", filepath.Join(makerelease, "darwin/Resources"), "--package-path", pkgdest, targ) if err != nil { return err } targs = append(targs, targ) case "windows": // Create ZIP file. zip := filepath.Join(work, base+".zip") err = makeZip(zip, work) // Copy zip to target file. targ := base + ".zip" err = cp(targ, zip) if err != nil { return err } targs = append(targs, targ) // Create MSI installer. win := filepath.Join(runtime.GOROOT(), "misc/makerelease/windows") installer := filepath.Join(win, "installer.wxs") appfiles := filepath.Join(work, "AppFiles.wxs") msi := filepath.Join(work, "installer.msi") // Gather files. _, err = b.run(work, "heat", "dir", "go", "-nologo", "-gg", "-g1", "-srd", "-sfrag", "-cg", "AppFiles", "-template", "fragment", "-dr", "INSTALLDIR", "-var", "var.SourceDir", "-out", appfiles) if err != nil { return err } // Build package. _, err = b.run(work, "candle", "-nologo", "-dVersion="+version, "-dArch="+b.Arch, "-dSourceDir=go", installer, appfiles) if err != nil { return err } appfiles = filepath.Join(work, "AppFiles.wixobj") installer = filepath.Join(work, "installer.wixobj") _, err = b.run(win, "light", "-nologo", "-ext", "WixUIExtension", "-ext", "WixUtilExtension", installer, appfiles, "-o", msi) if err != nil { return err } // Copy installer to target file. targ = base + ".msi" err = cp(targ, msi) targs = append(targs, targ) } if err == nil && *upload { for _, targ := range targs { err = b.Upload(version, targ) if err != nil { return fmt.Errorf("uploading %s: %v", targ, err) } } } return err } // extras fetches the go.tools, go.blog, and go-tour repositories, // builds them and copies the resulting binaries and static assets // to the new GOROOT. func (b *Build) extras() error { defer b.cleanGopath() if err := b.tools(); err != nil { return err } if err := b.blog(); err != nil { return err } return b.tour() } func (b *Build) get(repoPath, revision string) error { // Fetch the packages (without building/installing). _, err := b.run(b.gopath, filepath.Join(b.root, "bin", "go"), "get", "-d", repoPath+"/...") if err != nil { return err } // Update the repo to the specified revision. p := filepath.Join(b.gopath, "src", filepath.FromSlash(repoPath)) _, err = b.run(p, "hg", "update", revision) return err } func (b *Build) tools() error { // Fetch the go.tools repository. if err := b.get(toolPath, *toolTag); err != nil { return err } // Install tools. args := append([]string{"install"}, toolPaths...) _, err := b.run(b.gopath, filepath.Join(b.root, "bin", "go"), args...) if err != nil { return err } // Copy doc.go from go.tools/cmd/$CMD to $GOROOT/src/cmd/$CMD // while rewriting "package main" to "package documentation". for _, p := range toolPaths { d, err := ioutil.ReadFile(filepath.Join(b.gopath, "src", filepath.FromSlash(p), "doc.go")) if err != nil { return err } d = bytes.Replace(d, []byte("\npackage main\n"), []byte("\npackage documentation\n"), 1) cmdDir := filepath.Join(b.root, "src", "cmd", path.Base(p)) if err := os.MkdirAll(cmdDir, 0755); err != nil { return err } docGo := filepath.Join(cmdDir, "doc.go") if err := ioutil.WriteFile(docGo, d, 0644); err != nil { return err } } return nil } func (b *Build) blog() error { // Fetch the blog repository. _, err := b.run(b.gopath, filepath.Join(b.root, "bin", "go"), "get", "-d", blogPath+"/blog") if err != nil { return err } // Copy blog content to $GOROOT/blog. blogSrc := filepath.Join(b.gopath, "src", filepath.FromSlash(blogPath)) contentDir := filepath.Join(b.root, "blog") return cpAllDir(contentDir, blogSrc, blogContent...) } func (b *Build) tour() error { // Fetch the go-tour repository. if err := b.get(tourPath, *tourTag); err != nil { return err } // Build tour binary. _, err := b.run(b.gopath, filepath.Join(b.root, "bin", "go"), "install", tourPath+"/gotour") if err != nil { return err } // Copy all the tour content to $GOROOT/misc/tour. importPath := filepath.FromSlash(tourPath) tourSrc := filepath.Join(b.gopath, "src", importPath) contentDir := filepath.Join(b.root, "misc", "tour") if err = cpAllDir(contentDir, tourSrc, tourContent...); err != nil { return err } // Copy the tour source code so it's accessible with $GOPATH pointing to $GOROOT/misc/tour. if err = cpAllDir(filepath.Join(contentDir, "src", importPath), tourSrc, tourPackages...); err != nil { return err } // Copy gotour binary to tool directory as "tour"; invoked as "go tool tour". return cp( filepath.Join(b.root, "pkg", "tool", b.OS+"_"+b.Arch, "tour"+ext()), filepath.Join(b.gopath, "bin", "gotour"+ext()), ) } func (b *Build) cleanGopath() { for _, d := range []string{"bin", "pkg", "src"} { os.RemoveAll(filepath.Join(b.gopath, d)) } } func ext() string { if runtime.GOOS == "windows" { return ".exe" } return "" } func (b *Build) hgCmd(dir string, args ...string) ([]byte, error) { return b.run(dir, "hg", append([]string{"--config", "extensions.codereview=!"}, args...)...) } func (b *Build) run(dir, name string, args ...string) ([]byte, error) { buf := new(bytes.Buffer) absName, err := lookPath(name) if err != nil { return nil, err } cmd := exec.Command(absName, args...) var output io.Writer = buf if *verbose { log.Printf("Running %q %q", absName, args) output = io.MultiWriter(buf, os.Stdout) } cmd.Stdout = output cmd.Stderr = output cmd.Dir = dir cmd.Env = b.env() if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "%s", buf.Bytes()) return nil, fmt.Errorf("%s %s: %v", name, strings.Join(args, " "), err) } return buf.Bytes(), nil } var cleanEnv = []string{ "GOARCH", "GOBIN", "GOHOSTARCH", "GOHOSTOS", "GOOS", "GOROOT", "GOROOT_FINAL", "GOPATH", } func (b *Build) env() []string { env := os.Environ() for i := 0; i < len(env); i++ { for _, c := range cleanEnv { if strings.HasPrefix(env[i], c+"=") { env = append(env[:i], env[i+1:]...) } } } final := "/usr/local/go" if b.OS == "windows" { final = `c:\go` } env = append(env, "GOARCH="+b.Arch, "GOHOSTARCH="+b.Arch, "GOHOSTOS="+b.OS, "GOOS="+b.OS, "GOROOT="+b.root, "GOROOT_FINAL="+final, "GOPATH="+b.gopath, ) if b.static { env = append(env, "GO_DISTFLAGS=-s") } return env } func (b *Build) Upload(version string, filename string) error { file, err := ioutil.ReadFile(filename) if err != nil { return err } svc, err := storage.New(oauthClient) if err != nil { return err } obj := &storage.Object{ Acl: []*storage.ObjectAccessControl{{Entity: "allUsers", Role: "READER"}}, Name: filename, } _, err = svc.Objects.Insert(*storageBucket, obj).Media(bytes.NewReader(file)).Do() if err != nil { return err } sum := fmt.Sprintf("%x", sha1.Sum(file)) kind := "unknown" switch { case b.Source: kind = "source" case strings.HasSuffix(filename, ".tar.gz"), strings.HasSuffix(filename, ".zip"): kind = "archive" case strings.HasSuffix(filename, ".msi"), strings.HasSuffix(filename, ".pkg"): kind = "installer" } req, err := json.Marshal(File{ Filename: filename, Version: version, OS: b.OS, Arch: b.Arch, Checksum: sum, Kind: kind, }) if err != nil { return err } u := fmt.Sprintf("%s?%s", *uploadURL, url.Values{"key": []string{builderKey}}.Encode()) resp, err := http.Post(u, "application/json", bytes.NewReader(req)) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("upload status: %v", resp.Status) } return nil } type File struct { Filename string OS string Arch string Version string Checksum string `datastore:",noindex"` Kind string // "archive", "installer", "source" } func setupOAuthClient() error { config := &oauth.Config{ ClientId: "999119582588-h7kpj5pcm6d9solh5lgrbusmvvk4m9dn.apps.googleusercontent.com", ClientSecret: "8YLFgOhXIELWbO-NtF3iqIQz", Scope: storage.DevstorageRead_writeScope, AuthURL: "https://accounts.google.com/o/oauth2/auth", TokenURL: "https://accounts.google.com/o/oauth2/token", TokenCache: oauth.CacheFile(*tokenCache), RedirectURL: "oob", } transport := &oauth.Transport{Config: config} if token, err := config.TokenCache.Token(); err != nil { url := transport.Config.AuthCodeURL("") fmt.Println("Visit the following URL, obtain an authentication" + "code, and enter it below.") fmt.Println(url) fmt.Print("Enter authentication code: ") code := "" if _, err := fmt.Scan(&code); err != nil { return err } if _, err := transport.Exchange(code); err != nil { return err } } else { transport.Token = token } oauthClient = transport.Client() return nil } func (b *Build) clean(files []string) error { for _, name := range files { err := os.RemoveAll(filepath.Join(b.root, name)) if err != nil { return err } } return nil } func exists(path string) bool { _, err := os.Stat(path) return err == nil } func readCredentials() error { name := os.Getenv("HOME") if runtime.GOOS == "windows" { name = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") } name = filepath.Join(name, ".gobuildkey") f, err := os.Open(name) if err != nil { return err } defer f.Close() s := bufio.NewScanner(f) if s.Scan() { builderKey = s.Text() } return s.Err() } func cp(dst, src string) error { sf, err := os.Open(src) if err != nil { return err } defer sf.Close() fi, err := sf.Stat() if err != nil { return err } df, err := os.Create(dst) if err != nil { return err } defer df.Close() // Windows doesn't currently implement Fchmod if runtime.GOOS != "windows" { if err := df.Chmod(fi.Mode()); err != nil { return err } } _, err = io.Copy(df, sf) return err } func cpDir(dst, src string) error { walk := func(srcPath string, info os.FileInfo, err error) error { if err != nil { return err } dstPath := filepath.Join(dst, srcPath[len(src):]) if info.IsDir() { return os.MkdirAll(dstPath, 0755) } return cp(dstPath, srcPath) } return filepath.Walk(src, walk) } func cpAllDir(dst, basePath string, dirs ...string) error { for _, dir := range dirs { if err := cpDir(filepath.Join(dst, dir), filepath.Join(basePath, dir)); err != nil { return err } } return nil } func makeTar(targ, workdir string) error { f, err := os.Create(targ) if err != nil { return err } zout := gzip.NewWriter(f) tw := tar.NewWriter(zout) err = filepath.Walk(workdir, func(path string, fi os.FileInfo, err error) error { if !strings.HasPrefix(path, workdir) { log.Panicf("walked filename %q doesn't begin with workdir %q", path, workdir) } name := path[len(workdir):] // Chop of any leading / from filename, leftover from removing workdir. if strings.HasPrefix(name, "/") { name = name[1:] } // Don't include things outside of the go subdirectory (for instance, // the zip file that we're currently writing here.) if !strings.HasPrefix(name, "go/") { return nil } if *verbose { log.Printf("adding to tar: %s", name) } target, _ := os.Readlink(path) hdr, err := tar.FileInfoHeader(fi, target) if err != nil { return err } hdr.Name = name hdr.Uname = "root" hdr.Gname = "root" hdr.Uid = 0 hdr.Gid = 0 // Force permissions to 0755 for executables, 0644 for everything else. if fi.Mode().Perm()&0111 != 0 { hdr.Mode = hdr.Mode&^0777 | 0755 } else { hdr.Mode = hdr.Mode&^0777 | 0644 } err = tw.WriteHeader(hdr) if err != nil { return fmt.Errorf("Error writing file %q: %v", name, err) } if fi.IsDir() { return nil } r, err := os.Open(path) if err != nil { return err } defer r.Close() _, err = io.Copy(tw, r) return err }) if err != nil { return err } if err := tw.Close(); err != nil { return err } if err := zout.Close(); err != nil { return err } return f.Close() } func makeZip(targ, workdir string) error { f, err := os.Create(targ) if err != nil { return err } zw := zip.NewWriter(f) err = filepath.Walk(workdir, func(path string, fi os.FileInfo, err error) error { if !strings.HasPrefix(path, workdir) { log.Panicf("walked filename %q doesn't begin with workdir %q", path, workdir) } name := path[len(workdir):] // Convert to Unix-style named paths, as that's the // type of zip file that archive/zip creates. name = strings.Replace(name, "\\", "/", -1) // Chop of any leading / from filename, leftover from removing workdir. if strings.HasPrefix(name, "/") { name = name[1:] } // Don't include things outside of the go subdirectory (for instance, // the zip file that we're currently writing here.) if !strings.HasPrefix(name, "go/") { return nil } if *verbose { log.Printf("adding to zip: %s", name) } fh, err := zip.FileInfoHeader(fi) if err != nil { return err } fh.Name = name fh.Method = zip.Deflate if fi.IsDir() { fh.Name += "/" // append trailing slash fh.Method = zip.Store // no need to deflate 0 byte files } w, err := zw.CreateHeader(fh) if err != nil { return err } if fi.IsDir() { return nil } r, err := os.Open(path) if err != nil { return err } defer r.Close() _, err = io.Copy(w, r) return err }) if err != nil { return err } if err := zw.Close(); err != nil { return err } return f.Close() } type tool struct { name string commonDirs []string } var wixTool = tool{ "http://wix.sourceforge.net/, version 3.5", []string{`C:\Program Files\Windows Installer XML v3.5\bin`, `C:\Program Files (x86)\Windows Installer XML v3.5\bin`}, } var hgTool = tool{ "http://mercurial.selenic.com/wiki/WindowsInstall", []string{`C:\Program Files\Mercurial`, `C:\Program Files (x86)\Mercurial`, }, } var gccTool = tool{ "Mingw gcc; http://sourceforge.net/projects/mingw/files/Installer/mingw-get-inst/", []string{`C:\Mingw\bin`}, } var windowsDeps = map[string]tool{ "gcc": gccTool, "heat": wixTool, "candle": wixTool, "light": wixTool, "cmd": {"Windows cmd.exe", nil}, "hg": hgTool, } func checkWindowsDeps() { for prog, help := range windowsDeps { absPath, err := lookPath(prog) if err != nil { log.Fatalf("Failed to find necessary binary %q in path or common locations; %s", prog, help) } if *verbose { log.Printf("found windows dep %s at %s", prog, absPath) } } } func lookPath(prog string) (absPath string, err error) { absPath, err = exec.LookPath(prog) if err == nil { return } t, ok := windowsDeps[prog] if !ok { return } for _, dir := range t.commonDirs { for _, ext := range []string{"exe", "bat"} { absPath = filepath.Join(dir, prog+"."+ext) if _, err1 := os.Stat(absPath); err1 == nil { err = nil os.Setenv("PATH", os.Getenv("PATH")+";"+dir) return } } } return }