Make fsverity files able to be built independently

Right now, fsverity files are built when building the staging directory.
This is slow, before this cl it takes over a minute to generate the
staging directory, after it's only ~10 seconds.

Fsverity files are now generated before the staging directory is built,
in independant ninja actions.

Bug: 394404628
Test: m aosp_shared_system_image, verified it didn't change before/after

Change-Id: I9ebc40cd243888e3cb778487751937ccd79093bd
diff --git a/android/packaging.go b/android/packaging.go
index d216c0c..c2b0c1d 100644
--- a/android/packaging.go
+++ b/android/packaging.go
@@ -202,6 +202,11 @@
 	return p.srcPath
 }
 
+// The symlink target of the PackagingSpec. Do not use, for the soong-only migration.
+func (p *PackagingSpec) SymlinkTarget() string {
+	return p.symlinkTarget
+}
+
 type PackageModule interface {
 	Module
 	packagingBase() *PackagingBase
diff --git a/filesystem/fsverity_metadata.go b/filesystem/fsverity_metadata.go
index a3a2086..478fea9 100644
--- a/filesystem/fsverity_metadata.go
+++ b/filesystem/fsverity_metadata.go
@@ -21,9 +21,27 @@
 
 	"android/soong/android"
 
+	"github.com/google/blueprint"
 	"github.com/google/blueprint/proptools"
 )
 
+func init() {
+	pctx.HostBinToolVariable("fsverity_metadata_generator", "fsverity_metadata_generator")
+	pctx.HostBinToolVariable("fsverity_manifest_generator", "fsverity_manifest_generator")
+	pctx.HostBinToolVariable("fsverity", "fsverity")
+}
+
+var (
+	buildFsverityMeta = pctx.AndroidStaticRule("build_fsverity_meta", blueprint.RuleParams{
+		Command:     `$fsverity_metadata_generator --fsverity-path $fsverity --signature none --hash-alg sha256 --output $out $in`,
+		CommandDeps: []string{"$fsverity_metadata_generator", "$fsverity"},
+	})
+	buildFsverityManifest = pctx.AndroidStaticRule("build_fsverity_manifest", blueprint.RuleParams{
+		Command:     `$fsverity_manifest_generator --fsverity-path $fsverity --output $out @$in`,
+		CommandDeps: []string{"$fsverity_manifest_generator", "$fsverity"},
+	})
+)
+
 type fsverityProperties struct {
 	// Patterns of files for fsverity metadata generation.  For each matched file, a .fsv_meta file
 	// will be generated and included to the filesystem image.
@@ -35,13 +53,57 @@
 	Libs proptools.Configurable[[]string] `android:"path"`
 }
 
-func (f *filesystem) writeManifestGeneratorListFile(ctx android.ModuleContext, outputPath android.WritablePath, matchedSpecs []android.PackagingSpec, rebasedDir android.OutputPath) {
+// Mapping of a given fsverity file, which may be a real file or a symlink, and the on-device
+// path it should have relative to the filesystem root.
+type fsveritySrcDest struct {
+	src  android.Path
+	dest string
+}
+
+func (f *filesystem) writeManifestGeneratorListFile(
+	ctx android.ModuleContext,
+	outputPath android.WritablePath,
+	matchedFiles []fsveritySrcDest,
+	rootDir android.OutputPath,
+	rebasedDir android.OutputPath,
+) []android.Path {
+	prefix, err := filepath.Rel(rootDir.String(), rebasedDir.String())
+	if err != nil {
+		panic("rebasedDir should be relative to rootDir")
+	}
+	if prefix == "." {
+		prefix = ""
+	}
+	if f.PartitionType() == "system_ext" {
+		// Use the equivalent of $PRODUCT_OUT as the base dir.
+		// This ensures that the paths in build_manifest.pb contain on-device paths
+		// e.g. system_ext/framework/javalib.jar
+		// and not framework/javalib.jar.
+		//
+		// Although base-dir is outside the rootdir provided for packaging, this action
+		// is hermetic since it uses `manifestGeneratorListPath` to filter the files to be written to build_manifest.pb
+		prefix = "system_ext"
+	}
+
+	var deps []android.Path
 	var buf strings.Builder
-	for _, spec := range matchedSpecs {
-		buf.WriteString(rebasedDir.Join(ctx, spec.RelPathInPackage()).String())
-		buf.WriteRune('\n')
+	for _, spec := range matchedFiles {
+		src := spec.src.String()
+		dst := filepath.Join(prefix, spec.dest)
+		if strings.Contains(src, ",") {
+			ctx.ModuleErrorf("Path cannot contain a comma: %s", src)
+		}
+		if strings.Contains(dst, ",") {
+			ctx.ModuleErrorf("Path cannot contain a comma: %s", dst)
+		}
+		buf.WriteString(src)
+		buf.WriteString(",")
+		buf.WriteString(dst)
+		buf.WriteString("\n")
+		deps = append(deps, spec.src)
 	}
 	android.WriteFileRuleVerbatim(ctx, outputPath, buf.String())
+	return deps
 }
 
 func (f *filesystem) buildFsverityMetadataFiles(
@@ -64,69 +126,98 @@
 		return false
 	}
 
-	var matchedSpecs []android.PackagingSpec
+	var matchedFiles []android.PackagingSpec
+	var matchedSymlinks []android.PackagingSpec
 	for _, relPath := range android.SortedKeys(specs) {
 		if match(relPath) {
-			matchedSpecs = append(matchedSpecs, specs[relPath])
+			spec := specs[relPath]
+			if spec.SrcPath() != nil {
+				matchedFiles = append(matchedFiles, spec)
+			} else if spec.SymlinkTarget() != "" {
+				matchedSymlinks = append(matchedSymlinks, spec)
+			} else {
+				ctx.ModuleErrorf("Expected a file or symlink for fsverity packaging spec")
+			}
 		}
 	}
 
-	if len(matchedSpecs) == 0 {
+	if len(matchedFiles) == 0 && len(matchedSymlinks) == 0 {
 		return
 	}
 
-	fsverityPath := ctx.Config().HostToolPath(ctx, "fsverity")
-
 	// STEP 1: generate .fsv_meta
-	var sb strings.Builder
-	sb.WriteString("set -e\n")
-	for _, spec := range matchedSpecs {
+	var fsverityFileSpecs []fsveritySrcDest
+	for _, spec := range matchedFiles {
+		rel := spec.RelPathInPackage() + ".fsv_meta"
+		outPath := android.PathForModuleOut(ctx, "fsverity/meta_files", rel)
+		destPath := rebasedDir.Join(ctx, rel)
 		// srcPath is copied by CopySpecsToDir()
-		srcPath := rebasedDir.Join(ctx, spec.RelPathInPackage())
-		destPath := rebasedDir.Join(ctx, spec.RelPathInPackage()+".fsv_meta")
-		builder.Command().
-			BuiltTool("fsverity_metadata_generator").
-			FlagWithInput("--fsverity-path ", fsverityPath).
-			FlagWithArg("--signature ", "none").
-			FlagWithArg("--hash-alg ", "sha256").
-			FlagWithOutput("--output ", destPath).
-			Text(srcPath.String())
+		ctx.Build(pctx, android.BuildParams{
+			Rule:   buildFsverityMeta,
+			Input:  spec.SrcPath(),
+			Output: outPath,
+		})
+		builder.Command().Textf("cp").Input(outPath).Output(destPath)
 		f.appendToEntry(ctx, destPath)
 		*fullInstallPaths = append(*fullInstallPaths, FullInstallPathInfo{
 			SourcePath:      destPath,
-			FullInstallPath: android.PathForModuleInPartitionInstall(ctx, f.PartitionType(), spec.RelPathInPackage()+".fsv_meta"),
+			FullInstallPath: android.PathForModuleInPartitionInstall(ctx, f.PartitionType(), rel),
+		})
+		fsverityFileSpecs = append(fsverityFileSpecs, fsveritySrcDest{
+			src:  spec.SrcPath(),
+			dest: spec.RelPathInPackage(),
 		})
 	}
-
-	fsVerityBaseDir := rootDir.String()
-	if f.PartitionType() == "system_ext" {
-		// Use the equivalent of $PRODUCT_OUT as the base dir.
-		// This ensures that the paths in build_manifest.pb contain on-device paths
-		// e.g. system_ext/framework/javalib.jar
-		// and not framework/javalib.jar.
-		//
-		// Although base-dir is outside the rootdir provided for packaging, this action
-		// is hermetic since it uses `manifestGeneratorListPath` to filter the files to be written to build_manifest.pb
-		fsVerityBaseDir = filepath.Dir(rootDir.String())
+	for _, spec := range matchedSymlinks {
+		rel := spec.RelPathInPackage() + ".fsv_meta"
+		outPath := android.PathForModuleOut(ctx, "fsverity/meta_files", rel)
+		destPath := rebasedDir.Join(ctx, rel)
+		target := spec.SymlinkTarget() + ".fsv_meta"
+		ctx.Build(pctx, android.BuildParams{
+			Rule:   android.Symlink,
+			Output: outPath,
+			Args: map[string]string{
+				"fromPath": target,
+			},
+		})
+		builder.Command().
+			Textf("cp").
+			Flag(ctx.Config().CpPreserveSymlinksFlags()).
+			Input(outPath).
+			Output(destPath)
+		f.appendToEntry(ctx, destPath)
+		*fullInstallPaths = append(*fullInstallPaths, FullInstallPathInfo{
+			SymlinkTarget:   target,
+			FullInstallPath: android.PathForModuleInPartitionInstall(ctx, f.PartitionType(), rel),
+		})
+		// The fsverity manifest tool needs to actually look at the symlink. But symlink
+		// packagingSpecs are not actually created on disk, at least until the staging dir is
+		// built for the partition. Create a fake one now so the tool can see it.
+		realizedSymlink := android.PathForModuleOut(ctx, "fsverity/realized_symlinks", spec.RelPathInPackage())
+		ctx.Build(pctx, android.BuildParams{
+			Rule:   android.Symlink,
+			Output: realizedSymlink,
+			Args: map[string]string{
+				"fromPath": spec.SymlinkTarget(),
+			},
+		})
+		fsverityFileSpecs = append(fsverityFileSpecs, fsveritySrcDest{
+			src:  realizedSymlink,
+			dest: spec.RelPathInPackage(),
+		})
 	}
 
 	// STEP 2: generate signed BuildManifest.apk
 	// STEP 2-1: generate build_manifest.pb
-	manifestGeneratorListPath := android.PathForModuleOut(ctx, "fsverity_manifest.list")
-	f.writeManifestGeneratorListFile(ctx, manifestGeneratorListPath, matchedSpecs, rebasedDir)
-	assetsPath := android.PathForModuleOut(ctx, "fsverity_manifest/assets")
-	manifestPbPath := assetsPath.Join(ctx, "build_manifest.pb")
-	builder.Command().Text("rm -rf " + assetsPath.String())
-	builder.Command().Text("mkdir -p " + assetsPath.String())
-	builder.Command().
-		BuiltTool("fsverity_manifest_generator").
-		FlagWithInput("--fsverity-path ", fsverityPath).
-		FlagWithArg("--base-dir ", fsVerityBaseDir).
-		FlagWithArg("--output ", manifestPbPath.String()).
-		FlagWithInput("@", manifestGeneratorListPath)
-
-	f.appendToEntry(ctx, manifestPbPath)
-	f.appendToEntry(ctx, manifestGeneratorListPath)
+	manifestGeneratorListPath := android.PathForModuleOut(ctx, "fsverity/fsverity_manifest.list")
+	manifestDeps := f.writeManifestGeneratorListFile(ctx, manifestGeneratorListPath, fsverityFileSpecs, rootDir, rebasedDir)
+	manifestPbPath := android.PathForModuleOut(ctx, "fsverity/build_manifest.pb")
+	ctx.Build(pctx, android.BuildParams{
+		Rule:      buildFsverityManifest,
+		Input:     manifestGeneratorListPath,
+		Implicits: manifestDeps,
+		Output:    manifestPbPath,
+	})
 
 	// STEP 2-2: generate BuildManifest.apk (unsigned)
 	apkNameSuffix := ""
@@ -134,8 +225,8 @@
 		//https://source.corp.google.com/h/googleplex-android/platform/build/+/e392d2b486c2d4187b20a72b1c67cc737ecbcca5:core/Makefile;l=3410;drc=ea8f34bc1d6e63656b4ec32f2391e9d54b3ebb6b;bpv=1;bpt=0
 		apkNameSuffix = "SystemExt"
 	}
-	apkPath := rebasedDir.Join(ctx, "etc", "security", "fsverity", fmt.Sprintf("BuildManifest%s.apk", apkNameSuffix))
-	idsigPath := rebasedDir.Join(ctx, "etc", "security", "fsverity", fmt.Sprintf("BuildManifest%s.apk.idsig", apkNameSuffix))
+	apkPath := android.PathForModuleOut(ctx, "fsverity", fmt.Sprintf("BuildManifest%s.apk", apkNameSuffix))
+	idsigPath := android.PathForModuleOut(ctx, "fsverity", fmt.Sprintf("BuildManifest%s.apk.idsig", apkNameSuffix))
 	manifestTemplatePath := android.PathForSource(ctx, "system/security/fsverity/AndroidManifest.xml")
 	libs := android.PathsForModuleSrc(ctx, f.properties.Fsverity.Libs.GetOrDefault(ctx, nil))
 
@@ -144,12 +235,23 @@
 		minSdkVersion = ctx.Config().PlatformSdkVersion().String()
 	}
 
-	unsignedApkCommand := builder.Command().
-		Textf("mkdir -p %s && ", filepath.Dir(apkPath.String())).
+	apkBuilder := android.NewRuleBuilder(pctx, ctx)
+
+	// aapt2 doesn't support adding individual asset files. Create a temp directory to hold asset
+	// files and pass it to aapt2.
+	tmpAssetDir := android.PathForModuleOut(ctx, "fsverity/tmp_asset_dir")
+	stagedManifestPbPath := tmpAssetDir.Join(ctx, "build_manifest.pb")
+	apkBuilder.Command().
+		Text("rm -rf").Text(tmpAssetDir.String()).
+		Text("&&").
+		Text("mkdir -p").Text(tmpAssetDir.String())
+	apkBuilder.Command().Text("cp").Input(manifestPbPath).Output(stagedManifestPbPath)
+
+	unsignedApkCommand := apkBuilder.Command().
 		BuiltTool("aapt2").
 		Text("link").
 		FlagWithOutput("-o ", apkPath).
-		FlagWithArg("-A ", assetsPath.String())
+		FlagWithArg("-A ", tmpAssetDir.String()).Implicit(stagedManifestPbPath)
 	for _, lib := range libs {
 		unsignedApkCommand.FlagWithInput("-I ", lib)
 	}
@@ -159,26 +261,35 @@
 		FlagWithArg("--version-name ", ctx.Config().AppsDefaultVersionName()).
 		FlagWithInput("--manifest ", manifestTemplatePath).
 		Text(" --rename-manifest-package com.android.security.fsverity_metadata." + f.partitionName())
-	*fullInstallPaths = append(*fullInstallPaths, FullInstallPathInfo{
-		SourcePath:      apkPath,
-		FullInstallPath: android.PathForModuleInPartitionInstall(ctx, f.PartitionType(), fmt.Sprintf("etc/security/fsverity/BuildManifest%s.apk", apkNameSuffix)),
-	})
-
-	f.appendToEntry(ctx, apkPath)
 
 	// STEP 2-3: sign BuildManifest.apk
 	pemPath, keyPath := ctx.Config().DefaultAppCertificate(ctx)
-	builder.Command().
+	apkBuilder.Command().
 		BuiltTool("apksigner").
 		Text("sign").
 		FlagWithArg("--in ", apkPath.String()).
 		FlagWithInput("--cert ", pemPath).
 		FlagWithInput("--key ", keyPath).
 		ImplicitOutput(idsigPath)
+	apkBuilder.Build(fmt.Sprintf("%s_fsverity_apk", ctx.ModuleName()), "build fsverity apk")
+
+	// STEP 2-4: Install the apk into the staging directory
+	installedApkPath := rebasedDir.Join(ctx, "etc", "security", "fsverity", fmt.Sprintf("BuildManifest%s.apk", apkNameSuffix))
+	installedIdsigPath := rebasedDir.Join(ctx, "etc", "security", "fsverity", fmt.Sprintf("BuildManifest%s.apk.idsig", apkNameSuffix))
+	builder.Command().Text("cp").Input(apkPath).Text(installedApkPath.String())
+	builder.Command().Text("cp").Input(idsigPath).Text(installedIdsigPath.String())
+
+	*fullInstallPaths = append(*fullInstallPaths, FullInstallPathInfo{
+		SourcePath:      apkPath,
+		FullInstallPath: android.PathForModuleInPartitionInstall(ctx, f.PartitionType(), fmt.Sprintf("etc/security/fsverity/BuildManifest%s.apk", apkNameSuffix)),
+	})
+
+	f.appendToEntry(ctx, installedApkPath)
+
 	*fullInstallPaths = append(*fullInstallPaths, FullInstallPathInfo{
 		SourcePath:      idsigPath,
 		FullInstallPath: android.PathForModuleInPartitionInstall(ctx, f.PartitionType(), fmt.Sprintf("etc/security/fsverity/BuildManifest%s.apk.idsig", apkNameSuffix)),
 	})
 
-	f.appendToEntry(ctx, idsigPath)
+	f.appendToEntry(ctx, installedIdsigPath)
 }