1// Copyright 2015 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Choose which ninja file (stage) to run next
16//
17// In the common case, this program takes a list of ninja files, compares their
18// mtimes against their $file.timestamp mtimes, and picks the last up to date
19// ninja file to output. That stage is expected to rebuild the next file in the
20// list and call this program again. If none of the ninja files are considered
21// dirty, the last stage is output.
22//
23// One exception is if the current stage's ninja file was rewritten, it will be
24// run again.
25//
26// Another exception is if the source bootstrap file has been updated more
27// recently than the first stage, the source file will be copied to the first
28// stage, and output. This would be expected with a new source drop via git.
29// The timestamp of the first file is not updated so that it can be regenerated
30// with any local changes.
31
32package choosestage
33
34import (
35	"bytes"
36	"flag"
37	"fmt"
38	"io/ioutil"
39	"os"
40	"path/filepath"
41)
42
43var (
44	outputFile    string
45	currentFile   string
46	bootstrapFile string
47	verbose       bool
48)
49
50func init() {
51	flag.StringVar(&outputFile, "o", "", "Output file")
52	flag.StringVar(&currentFile, "current", "", "Current stage's file")
53	flag.StringVar(&bootstrapFile, "bootstrap", "", "Bootstrap file checked into source")
54	flag.BoolVar(&verbose, "v", false, "Verbose mode")
55}
56
57func compareFiles(a, b string) (bool, error) {
58	aData, err := ioutil.ReadFile(a)
59	if err != nil {
60		return false, err
61	}
62
63	bData, err := ioutil.ReadFile(b)
64	if err != nil {
65		return false, err
66	}
67
68	return bytes.Equal(aData, bData), nil
69}
70
71// If the source bootstrap reference file is newer, then we may have gotten
72// other source updates too. So we need to restart everything with the file
73// that was checked in instead of the bootstrap that we last built.
74func copyBootstrapIfNecessary(bootstrapFile, filename string) (bool, error) {
75	if bootstrapFile == "" {
76		return false, nil
77	}
78
79	bootstrapStat, err := os.Stat(bootstrapFile)
80	if err != nil {
81		return false, err
82	}
83
84	fileStat, err := os.Stat(filename)
85	if err != nil {
86		return false, err
87	}
88
89	time := fileStat.ModTime()
90	if !bootstrapStat.ModTime().After(time) {
91		return false, nil
92	}
93
94	fmt.Printf("Newer source version of %s. Copying to %s\n", filepath.Base(bootstrapFile), filepath.Base(filename))
95	if verbose {
96		fmt.Printf("Source: %s\nBuilt:  %s\n", bootstrapStat.ModTime(), time)
97	}
98
99	data, err := ioutil.ReadFile(bootstrapFile)
100	if err != nil {
101		return false, err
102	}
103
104	err = ioutil.WriteFile(filename, data, 0666)
105	if err != nil {
106		return false, err
107	}
108
109	// Restore timestamp to force regeneration of the bootstrap.ninja.in
110	err = os.Chtimes(filename, time, time)
111	return true, err
112}
113
114func main() {
115	flag.Parse()
116
117	if flag.NArg() == 0 {
118		fmt.Fprintf(os.Stderr, "Must specify at least one ninja file\n")
119		os.Exit(1)
120	}
121
122	if outputFile == "" {
123		fmt.Fprintf(os.Stderr, "Must specify an output file\n")
124		os.Exit(1)
125	}
126
127	gotoFile := flag.Arg(0)
128	if copied, err := copyBootstrapIfNecessary(bootstrapFile, flag.Arg(0)); err != nil {
129		fmt.Fprintf(os.Stderr, "Failed to copy bootstrap ninja file: %s\n", err)
130		os.Exit(1)
131	} else if !copied {
132		for _, fileName := range flag.Args() {
133			timestampName := fileName + ".timestamp"
134
135			// If we're currently running this stage, and the build.ninja.in
136			// file differs from the current stage file, then it has been rebuilt.
137			// Restart the stage.
138			if filepath.Clean(currentFile) == filepath.Clean(fileName) {
139				if _, err := os.Stat(outputFile); !os.IsNotExist(err) {
140					if ok, err := compareFiles(fileName, outputFile); err != nil {
141						fmt.Fprintf(os.Stderr, "Failure when comparing files: %s\n", err)
142						os.Exit(1)
143					} else if !ok {
144						fmt.Printf("Stage %s has changed, restarting\n", filepath.Base(fileName))
145						gotoFile = fileName
146						break
147					}
148				}
149			}
150
151			fileStat, err := os.Stat(fileName)
152			if err != nil {
153				// Regenerate this stage on error
154				break
155			}
156
157			timestampStat, err := os.Stat(timestampName)
158			if err != nil {
159				// This file may not exist. There's no point for
160				// the first stage to have one, as it should be
161				// a subset of the second stage dependencies,
162				// and both will return to the first stage.
163				continue
164			}
165
166			if verbose {
167				fmt.Printf("For %s:\n  file: %s\n  time: %s\n", fileName, fileStat.ModTime(), timestampStat.ModTime())
168			}
169
170			// If the timestamp file has a later modification time, that
171			// means that this stage needs to be regenerated. Break, so
172			// that we run the last found stage.
173			if timestampStat.ModTime().After(fileStat.ModTime()) {
174				break
175			}
176
177			gotoFile = fileName
178		}
179	}
180
181	fmt.Printf("Choosing %s for next stage\n", filepath.Base(gotoFile))
182
183	data, err := ioutil.ReadFile(gotoFile)
184	if err != nil {
185		fmt.Fprintf(os.Stderr, "Can't read file: %s", err)
186		os.Exit(1)
187	}
188
189	err = ioutil.WriteFile(outputFile, data, 0666)
190	if err != nil {
191		fmt.Fprintf(os.Stderr, "Can't write file: %s", err)
192		os.Exit(1)
193	}
194}
195