[build] Error out on abs paths in depfiles

Also fixed the last batch of offending targets/templates.

Fixed: 75451

Change-Id: I5f81f0b5543c6e729adcf833db81a464cfe8a78b
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/521540
Commit-Queue: Jay Zhuang <jayzhuang@google.com>
Reviewed-by: Shai Barack <shayba@google.com>
diff --git a/build/dart/dart_library.gni b/build/dart/dart_library.gni
index 673df43..d582bd1 100644
--- a/build/dart/dart_library.gni
+++ b/build/dart/dart_library.gni
@@ -342,7 +342,7 @@
                "--source_lists",
                "@" + rebase_path(_all_deps_sources_list_file, root_build_dir),
                "--sources",
-             ] + rebased_sources
+             ] + rebase_path(rebased_sources, root_build_dir)
 
       inputs = [ _all_deps_sources_list_file ]
       deps = [ ":${_all_deps_sources_list_target}" ]
@@ -443,21 +443,21 @@
 
         args = [
           "--source-file",
-          rebase_path(source_file),
+          rebase_path(source_file, root_build_dir),
           "--dot-packages",
-          rebase_path(_packages_path),
+          rebase_path(_packages_path, root_build_dir),
           "--dartanalyzer",
-          rebase_path(dart_analyzer_binary),
+          rebase_path(dart_analyzer_binary, root_build_dir),
           "--dart-sdk",
-          rebase_path(dart_sdk),
+          rebase_path(dart_sdk, root_build_dir),
           "--options",
-          rebase_path(options_file),
+          rebase_path(options_file, root_build_dir),
           "--stamp",
-          rebase_path(output_file),
+          rebase_path(output_file, root_build_dir),
           "--depname",
           rebase_path(output_file, root_build_dir),
           "--depfile",
-          rebase_path(depfile),
+          rebase_path(depfile, root_build_dir),
           "--all-deps-sources-file",
           rebase_path(_all_deps_sources_file, root_build_dir),
         ]
diff --git a/build/dart/run_analysis.py b/build/dart/run_analysis.py
index e5bbf19..1e1be3a 100755
--- a/build/dart/run_analysis.py
+++ b/build/dart/run_analysis.py
@@ -57,9 +57,6 @@
 
     options = args.options
     while True:
-        if not os.path.isabs(options):
-            print('Expected absolute path, got {}'.format(options))
-            return 1
         if not os.path.exists(options):
             print('Could not find options file: {}'.format(options))
             return 1
diff --git a/build/go/build.py b/build/go/build.py
index 0e9cae4..7eb15dd 100755
--- a/build/go/build.py
+++ b/build/go/build.py
@@ -383,7 +383,7 @@
         output = subprocess.check_output(
             go_list_args, env=env, cwd=gopath_src, text=True)
         with open(args.depfile, 'w') as into:
-            into.write(dist)
+            into.write(os.path.relpath(dist))
             into.write(':')
             while output:
                 try:
@@ -419,6 +419,7 @@
                         src_dir = t + src_dir[len(f):]
                         break
 
+                src_dir = os.path.relpath(src_dir)
                 for field in files_fields:
                     files = package.get(field)
                     if files:
diff --git a/build/go/go_build.gni b/build/go/go_build.gni
index 0f5aa206..469ef96 100644
--- a/build/go/go_build.gni
+++ b/build/go/go_build.gni
@@ -170,7 +170,7 @@
       "--root-out-dir",
       rebase_path(root_out_dir, root_build_dir),
       "--depfile",
-      rebase_path(depfile),
+      rebase_path(depfile, root_build_dir),
       "--current-cpu",
       current_cpu,
       "--current-os",
diff --git a/build/tracer/action_tracer.py b/build/tracer/action_tracer.py
index 815435b..bec5918 100755
--- a/build/tracer/action_tracer.py
+++ b/build/tracer/action_tracer.py
@@ -276,11 +276,118 @@
 
 
 @dataclasses.dataclass
+class DepEdges(object):
+    ins: FrozenSet[str] = dataclasses.field(default_factory=set)
+    outs: FrozenSet[str] = dataclasses.field(default_factory=set)
+
+    def abspaths(self) -> "DepEdges":
+        return DepEdges(ins=_abspaths(self.ins), outs=_abspaths(self.outs))
+
+
+def parse_dep_edges(depfile_line: str) -> DepEdges:
+    """Parse a single line of a depfile.
+
+    This assumes that all depfile entries are formatted onto a single line.
+    TODO(fangism): support more generalized forms of input, e.g. multi-line.
+      See https://github.com/ninja-build/ninja/blob/master/src/depfile_parser_test.cc
+
+    Args:
+      depfile_line: has the form "OUTPUT1 [OUTPUT2 ...]: INPUT [INPUT ...]"
+
+    Returns:
+      A DepEdges object represending a dependency between inputs and outputs.
+
+    Raises:
+      ValueError if unable to parse dependency entry.
+    """
+    outs, sep, ins = depfile_line.strip().partition(":")
+    if not sep:
+        raise ValueError("Failed to parse depfile entry:\n" + depfile_line)
+    return DepEdges(ins=set(shlex.split(ins)), outs=set(shlex.split(outs)))
+
+
+@dataclasses.dataclass
+class DepFile(object):
+    """DepFile represents a collection of dependency edges."""
+    deps: Collection[DepEdges] = dataclasses.field(default_factory=list)
+
+    @property
+    def all_ins(self) -> AbstractSet[str]:
+        """Returns a set of all dependency inputs."""
+        return {f for dep in self.deps for f in dep.ins}
+
+    @property
+    def all_outs(self) -> AbstractSet[str]:
+        """Returns a set of all dependency outputs."""
+        return {f for dep in self.deps for f in dep.outs}
+
+
+def parse_depfile(depfile_lines: Iterable[str]) -> DepFile:
+    """Parses a depfile into a set of inputs and outputs.
+
+    See https://github.com/ninja-build/ninja/blob/master/src/depfile_parser_test.cc
+    for examples of format using Ninja syntax.
+
+    TODO(fangism): ignore blank/comment lines
+
+    Args:
+      depfile_lines: lines from a depfile
+
+    Returns:
+      DepFile object, collection of dependencies.
+    """
+
+    # Go through all lines and join continuations. Doing this manually to avoid
+    # copies as much as possible.
+    lines = []
+    current_line = ""
+    for line in depfile_lines:
+        # We currently don't allow consecutive backslashes in filenames to
+        # simplify depfile parsing. Support can be added if use cases come up.
+        #
+        # Ninja's implementation:
+        # https://github.com/ninja-build/ninja/blob/5993141c0977f563de5e064fbbe617f9dc34bb8d/src/depfile_parser.cc#L39
+        if r"\\" in line:
+            raise ValueError(
+                f'Consecutive backslashes found in depfile line "{line}", this is not supported by action tracer'
+            )
+        # We currently don't have any use cases with trailing whitespaces in
+        # file names, so treat them as errors when they show up at the end of a
+        # line, because users usually want a line continuation. We can
+        # reconsider this check when use cases come up.
+        if trailing_white_spaces.match(line):
+            raise ValueError(
+                f'Backslash followed by trailing whitespaces at end of line "{line}", remove whitespaces for proper line continuation'
+            )
+
+        if line.endswith(("\\\n", "\\\r\n")):
+            current_line += line.rstrip("\\\r\n")
+            continue
+        current_line += line
+        lines.append(current_line)
+        current_line = ""
+
+    if current_line:
+        raise ValueError("Line continuation found at end of file")
+
+    return DepFile(deps=[parse_dep_edges(line) for line in lines])
+
+
+def abspaths_from_depfile(depfile: DepFile,
+                          allowed_abspaths: FrozenSet[str]) -> Collection[str]:
+    return [
+        f for f in (depfile.all_ins | depfile.all_outs)
+        if f not in allowed_abspaths and os.path.isabs(f)
+    ]
+
+
+@dataclasses.dataclass
 class Action(object):
     """Represents a set of parameters of a single build action."""
     inputs: Sequence[str] = dataclasses.field(default_factory=list)
     outputs: Collection[str] = dataclasses.field(default_factory=list)
     depfile: Optional[str] = None
+    parsed_depfile: Optional[DepFile] = None
     response_file_name: Optional[str] = None
 
     def access_constraints(
@@ -300,13 +407,13 @@
             allowed_writes.add(self.depfile)
             if os.path.exists(self.depfile):
                 with open(self.depfile, "r") as f:
-                    depfile = parse_depfile(f)
+                    self.parsed_depfile = parse_depfile(f)
 
                 if (writeable_depfile_inputs):
-                    allowed_writes.update(depfile.all_ins)
+                    allowed_writes.update(self.parsed_depfile.all_ins)
                 else:
-                    allowed_reads.update(depfile.all_ins)
-                allowed_writes.update(depfile.all_outs)
+                    allowed_reads.update(self.parsed_depfile.all_ins)
+                allowed_writes.update(self.parsed_depfile.all_outs)
 
         # Everything writeable is readable.
         allowed_reads.update(allowed_writes)
@@ -448,104 +555,6 @@
     }
 
 
-@dataclasses.dataclass
-class DepEdges(object):
-    ins: FrozenSet[str] = dataclasses.field(default_factory=set)
-    outs: FrozenSet[str] = dataclasses.field(default_factory=set)
-
-    def abspaths(self) -> "DepEdges":
-        return DepEdges(ins=_abspaths(self.ins), outs=_abspaths(self.outs))
-
-
-def parse_dep_edges(depfile_line: str) -> DepEdges:
-    """Parse a single line of a depfile.
-
-    This assumes that all depfile entries are formatted onto a single line.
-    TODO(fangism): support more generalized forms of input, e.g. multi-line.
-      See https://github.com/ninja-build/ninja/blob/master/src/depfile_parser_test.cc
-
-    Args:
-      depfile_line: has the form "OUTPUT1 [OUTPUT2 ...]: INPUT [INPUT ...]"
-
-    Returns:
-      A DepEdges object represending a dependency between inputs and outputs.
-
-    Raises:
-      ValueError if unable to parse dependency entry.
-    """
-    outs, sep, ins = depfile_line.strip().partition(":")
-    if sep != ":":
-        raise ValueError("Failed to parse depfile entry:\n" + depfile_line)
-    return DepEdges(ins=set(shlex.split(ins)), outs=set(shlex.split(outs)))
-
-
-@dataclasses.dataclass
-class DepFile(object):
-    """DepFile represents a collection of dependency edges."""
-    deps: Collection[DepEdges] = dataclasses.field(default_factory=list)
-
-    @property
-    def all_ins(self) -> AbstractSet[str]:
-        """Returns a set of all dependency inputs."""
-        return {f for dep in self.deps for f in dep.ins}
-
-    @property
-    def all_outs(self) -> AbstractSet[str]:
-        """Returns a set of all dependency outputs."""
-        return {f for dep in self.deps for f in dep.outs}
-
-
-def parse_depfile(depfile_lines: Iterable[str]) -> DepFile:
-    """Parses a depfile into a set of inputs and outputs.
-
-    See https://github.com/ninja-build/ninja/blob/master/src/depfile_parser_test.cc
-    for examples of format using Ninja syntax.
-
-    TODO(fangism): ignore blank/comment lines
-
-    Args:
-      depfile_lines: lines from a depfile
-
-    Returns:
-      DepFile object, collection of dependencies.
-    """
-
-    # Go through all lines and join continuations. Doing this manually to avoid
-    # copies as much as possible.
-    lines = []
-    current_line = []
-    for line in depfile_lines:
-        # We currently don't allow consecutive backslashes in filenames to
-        # simplify depfile parsing. Support can be added if use cases come up.
-        #
-        # Ninja's implementation:
-        # https://github.com/ninja-build/ninja/blob/5993141c0977f563de5e064fbbe617f9dc34bb8d/src/depfile_parser.cc#L39
-        if r"\\" in line:
-            raise ValueError(
-                f'Consecutive backslashes found in depfile line "{line}", this is not supported by action tracer'
-            )
-        # We currently don't have any use cases with trailing whitespaces in
-        # file names, so treat them as errors when they show up at the end of a
-        # line, because users usually want a line continuation. We can
-        # reconsider this check when use cases come up.
-        if trailing_white_spaces.match(line):
-            raise ValueError(
-                f'Backslash followed by trailing whitespaces at end of line "{line}", remove whitespaces for proper line continuation'
-            )
-
-        if line.endswith(("\\\n", "\\\r\n")):
-            current_line.append(line.rstrip("\\\r\n"))
-            continue
-        current_line.append(line)
-        lines.append("".join(current_line))
-        current_line = []
-
-    if current_line:
-        raise ValueError("Line continuation found at end of file")
-
-    return DepFile(deps=[parse_dep_edges(line) for line in lines])
-
-
 def _verbose_path(path: str) -> str:
     """When any symlinks are followed, show this."""
     realpath = os.path.realpath(path)
@@ -1002,6 +1011,22 @@
                     file=sys.stderr)
                 # do not set the exit code
 
+    if action.parsed_depfile:
+        allowed_abspaths = {"/usr/bin/env"}
+        abspaths = abspaths_from_depfile(
+            action.parsed_depfile, allowed_abspaths)
+
+        if abspaths:
+            exit_code = args.failed_check_status
+            one_path_per_line = '\n'.join(sorted(abspaths))
+            print(
+                f"""
+Found the following files with absolute paths in depfile {action.depfile} for {args.label}:
+
+{one_path_per_line}
+""",
+                file=sys.stderr)
+
     if retval != 0:
         # Always forward the action's non-zero exit code, regardless of tracer findings.
         return retval
diff --git a/build/zircon/sdk_build_id.py b/build/zircon/sdk_build_id.py
index 772f4db..5474f6d 100755
--- a/build/zircon/sdk_build_id.py
+++ b/build/zircon/sdk_build_id.py
@@ -81,7 +81,7 @@
     deps = []
     for dest, source in manifest.items():
         mappings.append(f'{dest}={source}')
-        deps.append(source)
+        deps.append(os.path.relpath(source))
     args.manifest.write('\n'.join(mappings))
     args.depfile.write(
         '{} {}: {}\n'.format(
diff --git a/src/sys/pkg/bin/pm/cmd/pm/publish/publish.go b/src/sys/pkg/bin/pm/cmd/pm/publish/publish.go
index 50a9286..b0d2d14 100644
--- a/src/sys/pkg/bin/pm/cmd/pm/publish/publish.go
+++ b/src/sys/pkg/bin/pm/cmd/pm/publish/publish.go
@@ -21,7 +21,7 @@
 	"go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/build"
 	"go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/pkg"
 	"go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/repo"
-	"go.fuchsia.dev/fuchsia/src/sys/pkg/lib/far/go"
+	far "go.fuchsia.dev/fuchsia/src/sys/pkg/lib/far/go"
 )
 
 const (
@@ -312,6 +312,19 @@
 	if *depfilePath != "" {
 		timestampPath := filepath.Join(config.RepoDir, "repository", "timestamp.json")
 		for i, str := range deps {
+			// Avoid absolute paths in depfiles.
+			if filepath.IsAbs(str) {
+				wd, err := os.Getwd()
+				if err != nil {
+					return fmt.Errorf("getting current working directory: %s", err)
+				}
+				rel, err := filepath.Rel(wd, str)
+				if err != nil {
+					return fmt.Errorf("rebasing %q to current working directory %q: %s", str, rel, err)
+				}
+				str = rel
+			}
+
 			// It is not clear if this is appropriate input for the depfile, which is
 			// underspecified - it's a "make format file". For the most part this should
 			// not affect Fuchsia builds, as we do not use "interesting" characters in
diff --git a/src/sys/pkg/bin/pm/cmd/pm/publish/publish_test.go b/src/sys/pkg/bin/pm/cmd/pm/publish/publish_test.go
index c95883a..4a29e91 100644
--- a/src/sys/pkg/bin/pm/cmd/pm/publish/publish_test.go
+++ b/src/sys/pkg/bin/pm/cmd/pm/publish/publish_test.go
@@ -60,8 +60,8 @@
 		t.Fatalf("got %#v, wanted one input", inputPaths)
 	}
 
-	if inputPaths[0] != archivePath {
-		t.Errorf("depfile inputs: %#v != %#v", inputPaths[0], archivePath)
+	if got, want := inputPaths[0], mustRelativePath(t, archivePath); got != want {
+		t.Errorf("depfile inputs: got %s, want %s", got, want)
 	}
 }
 
@@ -75,7 +75,7 @@
 	outputManifestPath := filepath.Join(cfg.OutputDir, "package_manifest.json")
 	packagesListPath := filepath.Join(cfg.OutputDir, "packages.list")
 
-	if err := ioutil.WriteFile(packagesListPath, []byte(outputManifestPath+"\n"), 0o600); err != nil {
+	if err := ioutil.WriteFile(packagesListPath, []byte(outputManifestPath+"\n"), 0600); err != nil {
 		t.Fatal(err)
 	}
 
@@ -91,11 +91,11 @@
 		t.Errorf("depfile output: got %q, want %q", outName, ex)
 	}
 
-	if inputPaths[0] != packagesListPath {
-		t.Errorf("depfile inputs: %q != %q", inputPaths[0], packagesListPath)
+	if got, want := inputPaths[0], mustRelativePath(t, packagesListPath); got != want {
+		t.Errorf("depfile inputs: got %q, want %q", got, want)
 	}
-	if inputPaths[1] != outputManifestPath {
-		t.Errorf("depfile inputs: %q != %q", inputPaths[1], outputManifestPath)
+	if got, want := inputPaths[1], mustRelativePath(t, outputManifestPath); got != want {
+		t.Errorf("depfile inputs: got %q, want %q", got, want)
 	}
 
 	inputPaths = inputPaths[2:]
@@ -110,17 +110,37 @@
 	}
 	sourcePaths := []string{}
 	for _, blob := range blobs {
-		sourcePaths = append(sourcePaths, blob.SourcePath)
+		sourcePaths = append(sourcePaths, mustRelativePath(t, blob.SourcePath))
 	}
 	sort.Strings(sourcePaths)
 
 	for i := range sourcePaths {
 		if inputPaths[i] != sourcePaths[i] {
-			t.Errorf("deps entry: %q != %q", inputPaths[i], sourcePaths[i])
+			t.Errorf("deps entry: got %q, want %q", inputPaths[i], sourcePaths[i])
 		}
 	}
 }
 
+// mustRelativePath converts the input path relative to the current working
+// directory. Input path is unchanged if it's not an absolute path.
+func mustRelativePath(t *testing.T, p string) string {
+	t.Helper()
+
+	if !filepath.IsAbs(p) {
+		return p
+	}
+
+	wd, err := os.Getwd()
+	if err != nil {
+		t.Fatalf("Failed to get current working directory: %v", err)
+	}
+	rel, err := filepath.Rel(wd, p)
+	if err != nil {
+		t.Fatalf("Failed to rebase %s to %s: %v", p, wd, err)
+	}
+	return rel
+}
+
 func readDepfile(t *testing.T, depfilePath string) (string, []string) {
 	b, err := ioutil.ReadFile(depfilePath)
 	if err != nil {