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, or in `exempt`
34func removeAbandonedFilesUnder(ctx *blueprint.Context,
35	srcDir, buildDir string, under, exempt []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	for _, target := range exempt {
61		replacedTarget := replacer.Replace(target)
62		targets[filepath.Clean(replacedTarget)] = true
63	}
64
65	filePaths, err := parseNinjaLog(ninjaBuildDir, under)
66	if err != nil {
67		return err
68	}
69
70	for _, filePath := range filePaths {
71		isTarget := targets[filePath]
72		if !isTarget {
73			err = removeFileAndEmptyDirs(absolutePath(filePath))
74			if err != nil {
75				return err
76			}
77		}
78	}
79
80	return nil
81}
82
83func parseNinjaLog(ninjaBuildDir string, under []string) ([]string, error) {
84	logFilePath := filepath.Join(ninjaBuildDir, logFileName)
85	logFile, err := os.Open(logFilePath)
86	if err != nil {
87		if os.IsNotExist(err) {
88			return nil, nil
89		}
90		return nil, err
91	}
92	defer logFile.Close()
93
94	scanner := bufio.NewScanner(logFile)
95
96	// Check that the first line indicates that this is a Ninja log version 5
97	const expectedFirstLine = "# ninja log v5"
98	if !scanner.Scan() || scanner.Text() != expectedFirstLine {
99		return nil, errors.New("unrecognized ninja log format")
100	}
101
102	var filePaths []string
103	for scanner.Scan() {
104		line := scanner.Text()
105		if strings.HasPrefix(line, "#") {
106			continue
107		}
108
109		const fieldSeperator = "\t"
110		fields := strings.Split(line, fieldSeperator)
111
112		const precedingFields = 3
113		const followingFields = 1
114
115		if len(fields) < precedingFields+followingFields+1 {
116			return nil, fmt.Errorf("log entry has too few fields: %q", line)
117		}
118
119		start := precedingFields
120		end := len(fields) - followingFields
121		filePath := strings.Join(fields[start:end], fieldSeperator)
122
123		for _, dir := range under {
124			if strings.HasPrefix(filePath, dir) {
125				filePaths = append(filePaths, filePath)
126				break
127			}
128		}
129	}
130	if err := scanner.Err(); err != nil {
131		return nil, err
132	}
133
134	return filePaths, nil
135}
136
137func removeFileAndEmptyDirs(path string) error {
138	err := os.Remove(path)
139	if err != nil {
140		if os.IsNotExist(err) {
141			return nil
142		}
143		pathErr := err.(*os.PathError)
144		switch pathErr.Err {
145		case syscall.ENOTEMPTY, syscall.EEXIST, syscall.ENOTDIR:
146			return nil
147		}
148		return err
149	}
150	fmt.Printf("removed old ninja-created file %s because it has no rule to generate it\n", path)
151
152	path, err = filepath.Abs(path)
153	if err != nil {
154		return err
155	}
156
157	cwd, err := os.Getwd()
158	if err != nil {
159		return err
160	}
161
162	for dir := filepath.Dir(path); dir != cwd; dir = filepath.Dir(dir) {
163		err = os.Remove(dir)
164		if err != nil {
165			pathErr := err.(*os.PathError)
166			switch pathErr.Err {
167			case syscall.ENOTEMPTY, syscall.EEXIST:
168				// We've come to a nonempty directory, so we're done.
169				return nil
170			default:
171				return err
172			}
173		}
174	}
175
176	return nil
177}
178