// Copyright 2014 Google Inc. All rights reserved.
//
// 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.
package bootstrap
import (
"bufio"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/google/blueprint"
)
const logFileName = ".ninja_log"
// removeAbandonedFilesUnder removes any files that appear in the Ninja log, and
// are prefixed with one of the `under` entries, but that are not currently
// build targets, or in `exempt`
func removeAbandonedFilesUnder(ctx *blueprint.Context, config *Config,
srcDir string, under, exempt []string) error {
if len(under) == 0 {
return nil
}
ninjaBuildDir, err := ctx.NinjaBuildDir()
if err != nil {
return err
}
targetRules, err := ctx.AllTargets()
if err != nil {
return fmt.Errorf("error determining target list: %s", err)
}
replacer := strings.NewReplacer(
"@@SrcDir@@", srcDir,
"@@BuildDir@@", BuildDir)
ninjaBuildDir = replacer.Replace(ninjaBuildDir)
targets := make(map[string]bool)
for target := range targetRules {
replacedTarget := replacer.Replace(target)
targets[filepath.Clean(replacedTarget)] = true
}
for _, target := range exempt {
replacedTarget := replacer.Replace(target)
targets[filepath.Clean(replacedTarget)] = true
}
filePaths, err := parseNinjaLog(ninjaBuildDir, under)
if err != nil {
return err
}
for _, filePath := range filePaths {
isTarget := targets[filePath]
if !isTarget {
err = removeFileAndEmptyDirs(filePath)
if err != nil {
return err
}
}
}
return nil
}
func parseNinjaLog(ninjaBuildDir string, under []string) ([]string, error) {
logFilePath := filepath.Join(ninjaBuildDir, logFileName)
logFile, err := os.Open(logFilePath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
defer logFile.Close()
scanner := bufio.NewScanner(logFile)
// Check that the first line indicates that this is a Ninja log version 5
const expectedFirstLine = "# ninja log v5"
if !scanner.Scan() || scanner.Text() != expectedFirstLine {
return nil, errors.New("unrecognized ninja log format")
}
var filePaths []string
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "#") {
continue
}
const fieldSeperator = "\t"
fields := strings.Split(line, fieldSeperator)
const precedingFields = 3
const followingFields = 1
if len(fields) < precedingFields+followingFields+1 {
return nil, fmt.Errorf("log entry has too few fields: %q", line)
}
start := precedingFields
end := len(fields) - followingFields
filePath := strings.Join(fields[start:end], fieldSeperator)
for _, dir := range under {
if strings.HasPrefix(filePath, dir) {
filePaths = append(filePaths, filePath)
break
}
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return filePaths, nil
}
func removeFileAndEmptyDirs(path string) error {
err := os.Remove(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
pathErr := err.(*os.PathError)
switch pathErr.Err {
case syscall.ENOTEMPTY, syscall.EEXIST, syscall.ENOTDIR:
return nil
}
return err
}
fmt.Printf("removed old ninja-created file %s because it has no rule to generate it\n", path)
path, err = filepath.Abs(path)
if err != nil {
return err
}
cwd, err := os.Getwd()
if err != nil {
return err
}
for dir := filepath.Dir(path); dir != cwd; dir = filepath.Dir(dir) {
err = os.Remove(dir)
if err != nil {
pathErr := err.(*os.PathError)
switch pathErr.Err {
case syscall.ENOTEMPTY, syscall.EEXIST:
// We've come to a nonempty directory, so we're done.
return nil
default:
return err
}
}
}
return nil
}