Build system_other image in soong-only builds

This just builds an empty system_other image for now, the dexpreopt
files will be added in a followup change.

Bug: 390269431
Test: m --soong-only
Change-Id: Ic4a9bcb8b7ba1eb4444b3339d6c0b0cdfd485714
diff --git a/android/variable.go b/android/variable.go
index 08bcedf..14094e2 100644
--- a/android/variable.go
+++ b/android/variable.go
@@ -623,6 +623,8 @@
 	VendorDlkmSecurityPatch string `json:",omitempty"`
 	OdmDlkmSecurityPatch    string `json:",omitempty"`
 
+	BuildingSystemOtherImage bool `json:",omitempty"`
+
 	// Boot image stuff
 	BuildingRamdiskImage              bool     `json:",omitempty"`
 	ProductBuildBootImage             bool     `json:",omitempty"`
diff --git a/filesystem/Android.bp b/filesystem/Android.bp
index 986b72e..cb76df2 100644
--- a/filesystem/Android.bp
+++ b/filesystem/Android.bp
@@ -28,6 +28,7 @@
         "raw_binary.go",
         "super_image.go",
         "system_image.go",
+        "system_other.go",
         "vbmeta.go",
         "testing.go",
     ],
diff --git a/filesystem/filesystem.go b/filesystem/filesystem.go
index ad19cc6..e485e4f 100644
--- a/filesystem/filesystem.go
+++ b/filesystem/filesystem.go
@@ -378,6 +378,11 @@
 	// Name of the module that produced this FilesystemInfo origionally. (though it may be
 	// re-exported by super images or boot images)
 	ModuleName string
+	// The property file generated by this module and passed to build_image.
+	// It's exported here so that system_other can reuse system's property file.
+	BuildImagePropFile android.Path
+	// Paths to all the tools referenced inside of the build image property file.
+	BuildImagePropFileDeps android.Paths
 }
 
 var FilesystemProvider = blueprint.NewProvider[FilesystemInfo]()
@@ -484,9 +489,11 @@
 
 	var mapFile android.Path
 	var outputHermetic android.Path
+	var buildImagePropFile android.Path
+	var buildImagePropFileDeps android.Paths
 	switch f.fsType(ctx) {
 	case ext4Type, erofsType, f2fsType:
-		f.output, outputHermetic = f.buildImageUsingBuildImage(ctx, builder, rootDir, rebasedDir)
+		f.output, outputHermetic, buildImagePropFile, buildImagePropFileDeps = f.buildImageUsingBuildImage(ctx, builder, rootDir, rebasedDir)
 		mapFile = f.getMapFile(ctx)
 	case compressedCpioType:
 		f.output = f.buildCpioImage(ctx, builder, rootDir, true)
@@ -508,17 +515,15 @@
 	android.WriteFileRule(ctx, fileListFile, f.installedFilesList())
 
 	fsInfo := FilesystemInfo{
-		Output:       f.output,
-		FileListFile: fileListFile,
-		RootDir:      rootDir,
-		RebasedDir:   rebasedDir,
-		ModuleName:   ctx.ModuleName(),
-	}
-	if mapFile != nil {
-		fsInfo.MapFile = mapFile
-	}
-	if outputHermetic != nil {
-		fsInfo.OutputHermetic = outputHermetic
+		Output:                 f.output,
+		OutputHermetic:         outputHermetic,
+		FileListFile:           fileListFile,
+		RootDir:                rootDir,
+		RebasedDir:             rebasedDir,
+		MapFile:                mapFile,
+		ModuleName:             ctx.ModuleName(),
+		BuildImagePropFile:     buildImagePropFile,
+		BuildImagePropFileDeps: buildImagePropFileDeps,
 	}
 
 	android.SetProvider(ctx, FilesystemProvider, fsInfo)
@@ -670,7 +675,7 @@
 	builder *android.RuleBuilder,
 	rootDir android.OutputPath,
 	rebasedDir android.OutputPath,
-) (android.Path, android.Path) {
+) (android.Path, android.Path, android.Path, android.Paths) {
 	// run host_init_verifier
 	// Ideally we should have a concept of pluggable linters that verify the generated image.
 	// While such concept is not implement this will do.
@@ -721,7 +726,7 @@
 	// rootDir is not deleted. Might be useful for quick inspection.
 	builder.Build("build_filesystem_image", fmt.Sprintf("Creating filesystem %s", f.BaseModuleName()))
 
-	return output, outputHermetic
+	return output, outputHermetic, propFile, toolDeps
 }
 
 func (f *filesystem) buildFileContexts(ctx android.ModuleContext) android.Path {
diff --git a/filesystem/super_image.go b/filesystem/super_image.go
index 4419a2f..5332462 100644
--- a/filesystem/super_image.go
+++ b/filesystem/super_image.go
@@ -18,6 +18,7 @@
 	"fmt"
 	"path/filepath"
 	"regexp"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -55,6 +56,9 @@
 	Sparse *bool
 	// information about how partitions within the super partition are grouped together
 	Partition_groups []PartitionGroupsInfo
+	// Name of the system_other partition filesystem module. This module will be installed to
+	// the "b" slot of the system partition in a/b partition builds.
+	System_other_partition *string
 	// whether dynamic partitions is used
 	Use_dynamic_partitions *bool
 	Virtual_ab             struct {
@@ -127,6 +131,12 @@
 
 var subImageDepTag superImageDepTagType
 
+type systemOtherDepTagType struct {
+	blueprint.BaseDependencyTag
+}
+
+var systemOtherDepTag systemOtherDepTagType
+
 func (s *superImage) DepsMutator(ctx android.BottomUpMutatorContext) {
 	addDependencyIfDefined := func(dep *string) {
 		if dep != nil {
@@ -143,6 +153,9 @@
 	addDependencyIfDefined(s.partitionProps.Vendor_dlkm_partition)
 	addDependencyIfDefined(s.partitionProps.Odm_partition)
 	addDependencyIfDefined(s.partitionProps.Odm_dlkm_partition)
+	if s.properties.System_other_partition != nil {
+		ctx.AddDependency(ctx.Module(), systemOtherDepTag, *s.properties.System_other_partition)
+	}
 }
 
 func (s *superImage) GenerateAndroidBuildActions(ctx android.ModuleContext) {
@@ -299,6 +312,20 @@
 		}
 	}
 
+	if s.properties.System_other_partition != nil {
+		if !slices.Contains(partitionList, "system") {
+			ctx.PropertyErrorf("system_other_partition", "Must have a system partition to use a system_other partition")
+		}
+		systemOther := ctx.GetDirectDepProxyWithTag(*s.properties.System_other_partition, systemOtherDepTag)
+		systemOtherFiles := android.OutputFilesForModule(ctx, systemOther, "")
+		if len(systemOtherFiles) != 1 {
+			ctx.PropertyErrorf("system_other_partition", "Expected 1 output file from module %q", *&s.properties.System_other_partition)
+		} else {
+			addStr("system_other_image", systemOtherFiles[0].String())
+			deps = append(deps, systemOtherFiles[0])
+		}
+	}
+
 	// Delay the error message until execution time because on aosp-main-future-without-vendor,
 	// BUILDING_VENDOR_IMAGE is false so we don't get the vendor image, but it's still listed in
 	// BOARD_GOOGLE_DYNAMIC_PARTITIONS_PARTITION_LIST.
diff --git a/filesystem/system_other.go b/filesystem/system_other.go
new file mode 100644
index 0000000..cad4263
--- /dev/null
+++ b/filesystem/system_other.go
@@ -0,0 +1,122 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// 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 filesystem
+
+import (
+	"android/soong/android"
+	"path/filepath"
+	"strings"
+
+	"github.com/google/blueprint"
+	"github.com/google/blueprint/proptools"
+)
+
+type SystemOtherImageProperties struct {
+	// The system_other image always requires a reference to the system image. The system_other
+	// partition gets built into the system partition's "b" slot in a/b partition builds. Thus, it
+	// copies most of its configuration from the system image, such as filesystem type, avb signing
+	// info, etc. Including it here does not automatically mean that it will pick up the system
+	// image's dexpropt files, it must also be listed in Preinstall_dexpreopt_files_from for that.
+	System_image *string
+
+	// This system_other partition will include all the dexpreopt files from the apps on these
+	// partitions.
+	Preinstall_dexpreopt_files_from []string
+}
+
+type systemOtherImage struct {
+	android.ModuleBase
+	android.DefaultableModuleBase
+	properties SystemOtherImageProperties
+}
+
+// The system_other image is the default contents of the "b" slot of the system image.
+// It contains the dexpreopt files of all the apps on the device, for a faster first boot.
+// Afterwards, at runtime, it will be used as a regular b slot for OTA updates, and the initial
+// dexpreopt files will be deleted.
+func SystemOtherImageFactory() android.Module {
+	module := &systemOtherImage{}
+	module.AddProperties(&module.properties)
+	android.InitAndroidMultiTargetsArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
+	return module
+}
+
+type systemImageDeptag struct {
+	blueprint.BaseDependencyTag
+}
+
+var systemImageDependencyTag = systemImageDeptag{}
+
+type dexpreoptDeptag struct {
+	blueprint.BaseDependencyTag
+}
+
+var dexpreoptDependencyTag = dexpreoptDeptag{}
+
+func (m *systemOtherImage) DepsMutator(ctx android.BottomUpMutatorContext) {
+	if proptools.String(m.properties.System_image) == "" {
+		ctx.ModuleErrorf("system_image property must be set")
+		return
+	}
+	ctx.AddDependency(ctx.Module(), systemImageDependencyTag, *m.properties.System_image)
+	ctx.AddDependency(ctx.Module(), dexpreoptDependencyTag, m.properties.Preinstall_dexpreopt_files_from...)
+}
+
+func (m *systemOtherImage) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+	systemImage := ctx.GetDirectDepProxyWithTag(*m.properties.System_image, systemImageDependencyTag)
+	systemInfo, ok := android.OtherModuleProvider(ctx, systemImage, FilesystemProvider)
+	if !ok {
+		ctx.PropertyErrorf("system_image", "Expected system_image module to provide FilesystemProvider")
+		return
+	}
+
+	output := android.PathForModuleOut(ctx, "system_other.img")
+	stagingDir := android.PathForModuleOut(ctx, "staging_dir")
+
+	builder := android.NewRuleBuilder(pctx, ctx)
+	builder.Command().Textf("rm -rf %s && mkdir -p %s", stagingDir, stagingDir)
+
+	for _, otherPartition := range m.properties.Preinstall_dexpreopt_files_from {
+		dexModule := ctx.GetDirectDepProxyWithTag(otherPartition, dexpreoptDependencyTag)
+		_, ok := android.OtherModuleProvider(ctx, dexModule, FilesystemProvider)
+		if !ok {
+			ctx.PropertyErrorf("preinstall_dexpreopt_files_from", "Expected module %q to provide FilesystemProvider", otherPartition)
+			return
+		}
+		// TODO(b/390269431): Install dex files to the staging dir
+	}
+
+	// Most of the time, if build_image were to call a host tool, it accepts the path to the
+	// host tool in a field in the prop file. However, it doesn't have that option for fec, which
+	// it expects to just be on the PATH. Add fec to the PATH.
+	fec := ctx.Config().HostToolPath(ctx, "fec")
+	pathToolDirs := []string{filepath.Dir(fec.String())}
+
+	builder.Command().
+		Textf("PATH=%s:$PATH", strings.Join(pathToolDirs, ":")).
+		BuiltTool("build_image").
+		Text(stagingDir.String()). // input directory
+		Input(systemInfo.BuildImagePropFile).
+		Implicits(systemInfo.BuildImagePropFileDeps).
+		Implicit(fec).
+		Output(output).
+		Text(stagingDir.String())
+
+	builder.Build("build_system_other", "build system other")
+
+	ctx.SetOutputFiles(android.Paths{output}, "")
+	ctx.CheckbuildFile(output)
+}
diff --git a/fsgen/filesystem_creator.go b/fsgen/filesystem_creator.go
index d00ebb2..6166074 100644
--- a/fsgen/filesystem_creator.go
+++ b/fsgen/filesystem_creator.go
@@ -156,6 +156,23 @@
 		}
 	}
 
+	var systemOtherImageName string
+	if buildingSystemOtherImage(partitionVars) {
+		systemModule := generatedModuleNameForPartition(ctx.Config(), "system")
+		systemOtherImageName = generatedModuleNameForPartition(ctx.Config(), "system_other")
+		ctx.CreateModule(
+			filesystem.SystemOtherImageFactory,
+			&filesystem.SystemOtherImageProperties{
+				System_image: &systemModule,
+			},
+			&struct {
+				Name *string
+			}{
+				Name: proptools.StringPtr(systemOtherImageName),
+			},
+		)
+	}
+
 	for _, x := range createVbmetaPartitions(ctx, finalSoongGeneratedPartitions) {
 		f.properties.Vbmeta_module_names = append(f.properties.Vbmeta_module_names, x.moduleName)
 		f.properties.Vbmeta_partition_names = append(f.properties.Vbmeta_partition_names, x.partitionName)
@@ -163,7 +180,7 @@
 
 	var superImageSubpartitions []string
 	if buildingSuperImage(partitionVars) {
-		superImageSubpartitions = createSuperImage(ctx, finalSoongGeneratedPartitions, partitionVars)
+		superImageSubpartitions = createSuperImage(ctx, finalSoongGeneratedPartitions, partitionVars, systemOtherImageName)
 		f.properties.Super_image = ":" + generatedModuleNameForPartition(ctx.Config(), "super")
 	}
 
@@ -183,6 +200,12 @@
 	return generatedModuleName(cfg, fmt.Sprintf("%s_image", partitionType))
 }
 
+func buildingSystemOtherImage(partitionVars android.PartitionVariables) bool {
+	// TODO: Recreate this logic from make instead of just depending on the final result variable:
+	// https://cs.android.com/android/platform/superproject/main/+/main:build/make/core/board_config.mk;l=429;drc=15a0df840e7093f65518003ab80cf24a3d9e8e6a
+	return partitionVars.BuildingSystemOtherImage
+}
+
 func (f *filesystemCreator) createBootloaderFilegroup(ctx android.LoadHookContext) (string, bool) {
 	bootloaderPath := ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse.PrebuiltBootloader
 	if len(bootloaderPath) == 0 {
diff --git a/fsgen/super_img.go b/fsgen/super_img.go
index 5c23868..e353688 100644
--- a/fsgen/super_img.go
+++ b/fsgen/super_img.go
@@ -27,7 +27,12 @@
 	return partitionVars.ProductBuildSuperPartition
 }
 
-func createSuperImage(ctx android.LoadHookContext, partitions []string, partitionVars android.PartitionVariables) []string {
+func createSuperImage(
+	ctx android.LoadHookContext,
+	partitions []string,
+	partitionVars android.PartitionVariables,
+	systemOtherImageName string,
+) []string {
 	baseProps := &struct {
 		Name *string
 	}{
@@ -79,6 +84,10 @@
 	}
 	superImageProps.Partition_groups = partitionGroupsInfo
 
+	if systemOtherImageName != "" {
+		superImageProps.System_other_partition = proptools.StringPtr(systemOtherImageName)
+	}
+
 	var superImageSubpartitions []string
 	partitionNameProps := &filesystem.SuperImagePartitionNameProperties{}
 	if android.InList("system", partitions) {