Reapply "Make fsverity files able to be built independently"

The difference from the original cl is an extra mkdir -p before
copying the apks to their staging dir path. The parent directories
weren't being created before.

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: Icf48f0859e6d66ba5ce68d77a7254218d271d526
diff --git a/android/packaging.go b/android/packaging.go
index 551fd4c..4e0c74a 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..89da318 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,36 @@
 		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("mkdir -p").Text(filepath.Dir(installedApkPath.String()))
+	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)
 }