1// Copyright 2014 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
15package bootstrap
16
17import (
18	"bufio"
19	"errors"
20	"fmt"
21	"os"
22	"path/filepath"
23	"strings"
24	"syscall"
25
26	"github.com/google/blueprint"
27)
28
29const logFileName = ".ninja_log"
30
31// removeAbandonedFilesUnder removes any files that appear in the Ninja log, and
32// are prefixed with one of the `under` entries, but that are not currently
33// build targets.
34func removeAbandonedFilesUnder(ctx *blueprint.Context, config *Config,
35	srcDir string, under []string) error {
36
37	if len(under) == 0 {
38		return nil
39	}
40
41	ninjaBuildDir, err := ctx.NinjaBuildDir()
42	if err != nil {
43		return err
44	}
45
46	targetRules, err := ctx.AllTargets()
47	if err != nil {
48		return fmt.Errorf("error determining target list: %s", err)
49	}
50
51	replacer := strings.NewReplacer(
52		"@@SrcDir@@", srcDir,
53		"@@BuildDir@@", BuildDir)
54	ninjaBuildDir = replacer.Replace(ninjaBuildDir)
55	targets := make(map[string]bool)
56	for target := range targetRules {
57		replacedTarget := replacer.Replace(target)
58		targets[filepath.Clean(replacedTarget)] = true
59	}
60
61	filePaths, err := parseNinjaLog(ninjaBuildDir, under)
62	if err != nil {
63		return err
64	}
65
66	for _, filePath := range filePaths {
67		isTarget := targets[filePath]
68		if !isTarget {
69			err = removeFileAndEmptyDirs(filePath)
70			if err != nil {
71				return err
72			}
73		}
74	}
75
76	return nil
77}
78
79func parseNinjaLog(ninjaBuildDir string, under []string) ([]string, error) {
80	logFilePath := filepath.Join(ninjaBuildDir, logFileName)
81	logFile, err := os.Open(logFilePath)
82	if err != nil {
83		if os.IsNotExist(err) {
84			return nil, nil
85		}
86		return nil, err
87	}
88	defer logFile.Close()
89
90	scanner := bufio.NewScanner(logFile)
91
92	// Check that the first line indicates that this is a Ninja log version 5
93	const expectedFirstLine = "# ninja log v5"
94	if !scanner.Scan() || scanner.Text() != expectedFirstLine {
95		return nil, errors.New("unrecognized ninja log format")
96	}
97
98	var filePaths []string
99	for scanner.Scan() {
100		line := scanner.Text()
101		if strings.HasPrefix(line, "#") {
102			continue
103		}
104
105		const fieldSeperator = "\t"
106		fields := strings.Split(line, fieldSeperator)
107
108		const precedingFields = 3
109		const followingFields = 1
110
111		if len(fields) < precedingFields+followingFields+1 {
112			return nil, fmt.Errorf("log entry has too few fields: %q", line)
113		}
114
115		start := precedingFields
116		end := len(fields) - followingFields
117		filePath := strings.Join(fields[start:end], fieldSeperator)
118
119		for _, dir := range under {
120			if strings.HasPrefix(filePath, dir) {
121				filePaths = append(filePaths, filePath)
122				break
123			}
124		}
125	}
126	if err := scanner.Err(); err != nil {
127		return nil, err
128	}
129
130	return filePaths, nil
131}
132
133func removeFileAndEmptyDirs(path string) error {
134	err := os.Remove(path)
135	if err != nil {
136		if os.IsNotExist(err) {
137			return nil
138		}
139		pathErr := err.(*os.PathError)
140		switch pathErr.Err {
141		case syscall.ENOTEMPTY, syscall.EEXIST, syscall.ENOTDIR:
142			return nil
143		}
144		return err
145	}
146	fmt.Printf("removed old ninja-created file %s because it has no rule to generate it\n", path)
147
148	path, err = filepath.Abs(path)
149	if err != nil {
150		return err
151	}
152
153	cwd, err := os.Getwd()
154	if err != nil {
155		return err
156	}
157
158	for dir := filepath.Dir(path); dir != cwd; dir = filepath.Dir(dir) {
159		err = os.Remove(dir)
160		if err != nil {
161			pathErr := err.(*os.PathError)
162			switch pathErr.Err {
163			case syscall.ENOTEMPTY, syscall.EEXIST:
164				// We've come to a nonempty directory, so we're done.
165				return nil
166			default:
167				return err
168			}
169		}
170	}
171
172	return nil
173}
174