blob: 16193bdc1de31594546653071a5c95473612b02f [file] [log] [blame]
// 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, OS X, and Windows.
package main
import (
"archive/tar"
"archive/zip"
"bufio"
"bytes"
"compress/gzip"
"encoding/base64"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
)
var (
tag = flag.String("tag", "weekly", "mercurial 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", true, "upload resulting files to Google Code")
wxsFile = flag.String("wxs", "", "path to custom installer.wxs")
addLabel = flag.String("label", "", "additional label to apply to file when uploading")
username, password string // for Google Code upload
)
const (
packageMaker = "/Applications/Utilities/PackageMaker.app/Contents/MacOS/PackageMaker"
uploadURL = "https://go.googlecode.com/files"
)
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 fileRe = regexp.MustCompile(`^go\.([a-z0-9-.]+)\.(src|([a-z0-9]+)-([a-z0-9]+))\.`)
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.Println("readCredentials:", 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]
}
if !*upload {
log.Printf("%s: -upload=false, skipping", targ)
continue
}
if err := b.Upload(version, targ); err != nil {
log.Printf("%s: %v", targ, err)
}
continue
}
if targ == "source" {
b.Source = true
} else {
p := strings.SplitN(targ, "-", 2)
if len(p) != 2 {
log.Println("Ignoring unrecognized target:", targ)
continue
}
b.OS = p[0]
b.Arch = p[1]
}
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
OS string
Arch string
root string
}
func (b *Build) Do() error {
work, err := ioutil.TempDir("", "bindist")
if err != nil {
return err
}
defer os.RemoveAll(work)
b.root = filepath.Join(work, "go")
// Clone Go distribution and update to tag.
_, err = b.run(work, "hg", "clone", "-q", *repo, b.root)
if err != nil {
return err
}
_, err = b.run(b.root, "hg", "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 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])
// 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("go.%s.%s-%s", version, b.OS, b.Arch)
var targs []string
switch b.OS {
case "linux", "freebsd", "":
// build tarball
targ := base
if b.Source {
targ = fmt.Sprintf("go.%s.src", version)
}
targ += ".tar.gz"
err = makeTar(targ, work)
targs = append(targs, targ)
case "darwin":
// arrange work so it's laid out as the dest filesystem
etc := filepath.Join(b.root, "misc/dist/darwin/etc")
_, err = b.run(work, "cp", "-r", etc, ".")
if err != nil {
return err
}
localDir := filepath.Join(work, "usr/local")
err = os.MkdirAll(localDir, 0744)
if err != nil {
return err
}
_, err = b.run(work, "mv", "go", localDir)
if err != nil {
return err
}
// build package
pm := packageMaker
if !exists(pm) {
pm = "/Developer" + pm
if !exists(pm) {
return errors.New("couldn't find PackageMaker")
}
}
targ := base + ".pkg"
scripts := filepath.Join(work, "usr/local/go/misc/dist/darwin/scripts")
_, err = b.run("", pm, "-v",
"-r", work,
"-o", targ,
"--scripts", scripts,
"--id", "com.googlecode.go",
"--title", "Go",
"--version", "1.0",
"--target", "10.6")
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(b.root, "misc/dist/windows")
installer := filepath.Join(win, "installer.wxs")
if *wxsFile != "" {
installer = *wxsFile
}
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 err
}
}
}
return err
}
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",
}
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,
)
return env
}
func (b *Build) Upload(version string, filename string) error {
// Prepare upload metadata.
var labels []string
os_, arch := b.OS, b.Arch
switch b.Arch {
case "386":
arch = "32-bit"
case "amd64":
arch = "64-bit"
}
if arch != "" {
labels = append(labels, "Arch-"+b.Arch)
}
switch b.OS {
case "linux":
os_ = "Linux"
labels = append(labels, "Type-Archive", "OpSys-Linux")
case "freebsd":
os_ = "FreeBSD"
labels = append(labels, "Type-Archive", "OpSys-FreeBSD")
case "darwin":
os_ = "Mac OS X"
labels = append(labels, "Type-Installer", "OpSys-OSX")
case "windows":
os_ = "Windows"
labels = append(labels, "OpSys-Windows")
}
summary := fmt.Sprintf("%s %s (%s)", version, os_, arch)
if b.OS == "windows" {
switch {
case strings.HasSuffix(filename, ".msi"):
labels = append(labels, "Type-Installer")
summary += " MSI installer"
case strings.HasSuffix(filename, ".zip"):
labels = append(labels, "Type-Archive")
summary += " ZIP archive"
}
}
if b.Source {
labels = append(labels, "Type-Source")
summary = fmt.Sprintf("%s (source only)", version)
}
if *addLabel != "" {
labels = append(labels, *addLabel)
}
// Put "Go" prefix on summary when it doesn't already begin with "go".
if !strings.HasPrefix(strings.ToLower(summary), "go") {
summary = "Go " + summary
}
// Open file to upload.
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
// Prepare multipart payload.
body := new(bytes.Buffer)
w := multipart.NewWriter(body)
if err := w.WriteField("summary", summary); err != nil {
return err
}
for _, l := range labels {
if err := w.WriteField("label", l); err != nil {
return err
}
}
fw, err := w.CreateFormFile("filename", filename)
if err != nil {
return err
}
if _, err = io.Copy(fw, f); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
// Send the file to Google Code.
req, err := http.NewRequest("POST", uploadURL, body)
if err != nil {
return err
}
token := fmt.Sprintf("%s:%s", username, password)
token = base64.StdEncoding.EncodeToString([]byte(token))
req.Header.Set("Authorization", "Basic "+token)
req.Header.Set("Content-type", w.FormDataContentType())
resp, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
return err
}
if resp.StatusCode/100 != 2 {
fmt.Fprintln(os.Stderr, "upload failed")
defer resp.Body.Close()
io.Copy(os.Stderr, resp.Body)
return fmt.Errorf("upload: %s", resp.Status)
}
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()
r := bufio.NewReader(f)
for i := 0; i < 3; i++ {
b, _, err := r.ReadLine()
if err != nil {
return err
}
b = bytes.TrimSpace(b)
switch i {
case 1:
username = string(b)
case 2:
password = string(b)
}
}
return nil
}
func cp(dst, src string) error {
sf, err := os.Open(src)
if err != nil {
return err
}
defer sf.Close()
df, err := os.Create(dst)
if err != nil {
return err
}
defer df.Close()
_, err = io.Copy(df, sf)
return err
}
func makeTar(targ, workdir string) error {
f, err := os.Create(targ)
if err != nil {
return err
}
zout := gzip.NewWriter(f)
tw := tar.NewWriter(zout)
filepath.Walk(workdir, filepath.WalkFunc(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)
}
if fi.IsDir() {
return nil
}
hdr, err := tarFileInfoHeader(fi, path)
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)
}
r, err := os.Open(path)
if err != nil {
return err
}
defer r.Close()
_, err = io.Copy(tw, r)
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)
filepath.Walk(workdir, filepath.WalkFunc(func(path string, fi os.FileInfo, err error) error {
if fi.IsDir() {
return nil
}
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
w, err := zw.CreateHeader(fh)
if err != nil {
return err
}
r, err := os.Open(path)
if err != nil {
return err
}
defer r.Close()
_, err = io.Copy(w, r)
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
}
// sysStat, if non-nil, populates h from system-dependent fields of fi.
var sysStat func(fi os.FileInfo, h *tar.Header) error
// Mode constants from the tar spec.
const (
c_ISDIR = 040000
c_ISFIFO = 010000
c_ISREG = 0100000
c_ISLNK = 0120000
c_ISBLK = 060000
c_ISCHR = 020000
c_ISSOCK = 0140000
)
// tarFileInfoHeader creates a partially-populated Header from an os.FileInfo.
// The filename parameter is used only in the case of symlinks, to call os.Readlink.
// If fi is a symlink but filename is empty, an error is returned.
func tarFileInfoHeader(fi os.FileInfo, filename string) (*tar.Header, error) {
h := &tar.Header{
Name: fi.Name(),
ModTime: fi.ModTime(),
Mode: int64(fi.Mode().Perm()), // or'd with c_IS* constants later
}
switch {
case fi.Mode()&os.ModeType == 0:
h.Mode |= c_ISREG
h.Typeflag = tar.TypeReg
h.Size = fi.Size()
case fi.IsDir():
h.Typeflag = tar.TypeDir
h.Mode |= c_ISDIR
case fi.Mode()&os.ModeSymlink != 0:
h.Typeflag = tar.TypeSymlink
h.Mode |= c_ISLNK
if filename == "" {
return h, fmt.Errorf("archive/tar: unable to populate Header.Linkname of symlinks")
}
targ, err := os.Readlink(filename)
if err != nil {
return h, err
}
h.Linkname = targ
case fi.Mode()&os.ModeDevice != 0:
if fi.Mode()&os.ModeCharDevice != 0 {
h.Mode |= c_ISCHR
h.Typeflag = tar.TypeChar
} else {
h.Mode |= c_ISBLK
h.Typeflag = tar.TypeBlock
}
case fi.Mode()&os.ModeSocket != 0:
h.Mode |= c_ISSOCK
default:
return nil, fmt.Errorf("archive/tar: unknown file mode %v", fi.Mode())
}
if sysStat != nil {
return h, sysStat(fi, h)
}
return h, nil
}