blob: 6de7db09e089455591649d13d420d834d5551453 [file] [log] [blame]
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// The do command wraps CMake, simplifying the building GAPID in common
// configurations.
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"os/exec"
"os/user"
"path/filepath"
"reflect"
"runtime"
"strings"
)
const (
cfgPath = ".gapid-config"
usage = `
go run do.go <verb>
verb can be one of the following:
• build <target> - start a build of the optional target
• config - set configuration parameters
• glob - update CMakeFiles.cmake
• clean - delete the output directory
• robo - runs an offline RoboTester instance
• help - show this message
`
// Changes to this version will force a full clean build.
// This is useful for situations where the CMake flags have changed and
// regenerating the files is required.
versionMajor = 1
versionMinor = 0 // For future use.
)
func main() {
flag.Parse()
verb := flag.Arg(0)
switch verb {
case "build":
doBuild(getConfig(), flag.Args()[1:]...)
case "config":
maxArgs(1)
cfg, _ := readConfig()
setConfig(cfg)
case "glob":
maxArgs(1)
doGlob(getConfig())
case "clean":
maxArgs(1)
doClean(getConfig())
case "robo", "robotester":
maxArgs(1)
doBuild(getConfig(), "run-robotester")
case "help":
showUsage()
return
case "":
showUsage()
os.Exit(0)
default:
fmt.Printf("Unrecognised verb %s", verb)
showUsage()
os.Exit(1)
}
}
func maxArgs(count int) {
if len(flag.Args()) > count {
showUsage()
os.Exit(1)
}
}
func showUsage() { fmt.Print(usage) }
// Config is the structure that holds all the configuration settings.
type Config struct {
Flavor string `enum:"release,debug" desc:"Build flavor"`
OutRoot string `desc:"Build output directory"`
CMakePath string `file:"true" desc:"Path to cmake executable"`
NinjaPath string `file:"true" desc:"Path to ninja executable"`
NDKRoot string `desc:"Path to NDK. (optional)"`
}
func (cfg Config) outputPath() string { return abs(cfg.OutRoot, cfg.Flavor) }
func (cfg Config) setCurrent() {
current := cfg.outputPath()
symlink(abs(cfg.OutRoot, "current"), current)
symlink(abs(cfg.OutRoot, "bin"), filepath.Join(current, "bin"))
symlink(abs(cfg.OutRoot, "pkg"), filepath.Join(current, "pkg"))
}
func (cfg Config) removeCurrent() {
os.Remove(abs(cfg.OutRoot, "current"))
os.Remove(abs(cfg.OutRoot, "bin"))
}
func (cfg Config) loadBuildVersion() (int, int) {
data, err := ioutil.ReadFile(filepath.Join(cfg.outputPath(), "do-version.txt"))
if err != nil {
return 0, 0
}
var major, minor int
fmt.Sscanf(string(data), "%d.%d", &major, &minor)
return major, minor
}
func (cfg Config) storeBuildVersion() {
str := fmt.Sprintf("%d.%d", versionMajor, versionMinor)
ioutil.WriteFile(filepath.Join(cfg.outputPath(), "do-version.txt"), []byte(str), 0666)
}
var defaults = Config{
Flavor: "release",
OutRoot: "~/gapid",
CMakePath: abs("../../../../../../../prebuilts/cmake/" + host() + "-x86/bin/cmake"), // TODO: change for target OS.
NinjaPath: abs("../../../../../../../prebuilts/ninja/" + host() + "-x86/ninja"), // TODO: change for target OS.
}
func host() string { return runtime.GOOS }
func lookPath(name string) string {
if path, err := exec.LookPath(name); err == nil {
return path
}
return name
}
func getConfig() Config {
cfg, cfgRead := readConfig()
if !cfgRead {
cfg = setConfig(cfg)
}
return cfg
}
func readConfig() (Config, bool) {
data, err := ioutil.ReadFile(cfgPath)
if err != nil {
return defaults, false
}
cfg := defaults
if err := json.Unmarshal(data, &cfg); err != nil {
return defaults, false
}
return cfg, true
}
func writeConfig(cfg Config) {
data, err := json.Marshal(cfg)
if err != nil {
panic(err)
}
if err := ioutil.WriteFile(cfgPath, data, 0666); err != nil {
panic(err)
}
}
func setConfig(cfg Config) Config {
v, t := reflect.ValueOf(&cfg).Elem(), reflect.TypeOf(cfg)
for i, c := 0, t.NumField(); i < c; i++ {
v, t := v.Field(i), t.Field(i)
desc := t.Tag.Get("desc")
if desc == "" {
desc = t.Name
}
enum := t.Tag.Get("enum")
var options []string
if enum != "" {
options = strings.Split(enum, ",")
desc += fmt.Sprintf(". One of: %v", strings.Join(options, ", "))
}
isFile := t.Tag.Get("file") == "true"
fmt.Printf(" • %s [Default: %v]\n", desc, v.Interface())
in := readLine()
switch t.Type.Kind() {
case reflect.String:
def := v.Interface().(string)
if in == "" {
in = def
}
if isFile && !exists(in) {
fmt.Printf("%v does not exist\n", in)
i--
continue
}
if options != nil && !isIn(in, options) {
fmt.Printf("Must be one of: %v\n", strings.Join(options, ", "))
i--
continue
}
v.Set(reflect.ValueOf(in))
case reflect.Bool:
val := v.Interface().(bool)
if in != "" {
var ok bool
if val, ok = parseBool(in); !ok {
fmt.Printf("Must be yes/true or no/false\n")
i--
continue
}
}
v.Set(reflect.ValueOf(val))
}
}
writeConfig(cfg)
outPath := cfg.outputPath()
mkdir(outPath)
cfg.setCurrent()
return cfg
}
func doGlob(cfg Config) {
cfg.setCurrent()
outPath := cfg.outputPath()
run(outPath, abs(cfg.CMakePath), "-DFORCE_GLOB=true", outPath)
}
func doClean(cfg Config) {
outPath := cfg.outputPath()
if exists(abs(outPath, "CMakeCache.txt")) {
os.RemoveAll(outPath)
}
cfg.removeCurrent()
}
func doBuild(cfg Config, targets ...string) {
outPath := cfg.outputPath()
runCMake := !exists(abs(outPath, "CMakeCache.txt"))
// Has the do major version changed since the last build?
lastMajor, _ := cfg.loadBuildVersion()
if !runCMake && lastMajor != versionMajor {
fmt.Printf("Major changes made to CMake. Performing a full rebuild.\n")
doClean(cfg)
runCMake = true
}
mkdir(outPath)
cfg.setCurrent()
cfg.storeBuildVersion()
if runCMake {
args := []string{
"-GNinja",
"-DCMAKE_MAKE_PROGRAM=" + abs(cfg.NinjaPath),
"-DCMAKE_BUILD_TYPE=" + strings.Title(cfg.Flavor),
"-DINSTALL_PREFIX=" + filepath.Join(outPath, "pkg"),
}
if cfg.NDKRoot != "" {
args = append(args, "-DANDROID_NDK_ROOT="+cfg.NDKRoot)
}
if runtime.GOOS == "darwin" {
args = append(args, "-DPREBUILTS=NO")
}
args = append(args, srcPath())
run(outPath, abs(cfg.CMakePath), args...)
run(outPath, abs(cfg.NinjaPath), targets...) // b/29146339
}
run(outPath, abs(cfg.NinjaPath), targets...)
if len(targets) == 0 {
run(outPath, abs(cfg.NinjaPath), "install")
}
}
func srcPath() string {
return abs(os.Getenv("GOPATH"), "src/android.googlesource.com/platform/tools/gpu")
}
func isIn(str string, list []string) bool {
for _, s := range list {
if s == str {
return true
}
}
return false
}
func readLine() string {
r := bufio.NewReader(os.Stdin)
l, _ := r.ReadString('\n')
return strings.Trim(l, "\n")
}
func parseBool(str string) (val, ok bool) {
switch strings.ToLower(str) {
case "yes", "y", "true":
return true, true
case "no", "n", "false":
return false, true
}
return false, false
}
func mkdir(path string) {
if err := os.MkdirAll(path, 0755); err != nil {
panic(err)
}
}
func symlink(dst, src string) {
os.Remove(dst)
if err := os.Symlink(src, dst); err != nil {
panic(err)
}
}
func exists(path string) bool {
info, _ := os.Stat(path)
return info != nil
}
func abs(path string, subdirs ...string) string {
if strings.HasPrefix(path, "~") {
u, err := user.Current()
if err != nil {
panic(err)
}
path = filepath.Join(u.HomeDir, strings.TrimLeft(path, "~"))
}
path, err := filepath.Abs(path)
if err != nil {
panic(err)
}
for _, s := range subdirs {
path = filepath.Join(path, s)
}
return path
}
func run(cwd string, exe string, args ...string) {
cmd := exec.Command(exe, args...)
cmd.Dir = cwd
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Error running %s: %v\n", exe, err)
os.Exit(1)
}
}