Build GKI APEX from $(PRODUCT_OUT)/boot*.img

Test: on CF build boot_5.4-android12-0 and inspect output
Test: on aosp_arm64 build boot-5.4_5.4-android12-0 and inspect output

Bug: 161563386
Change-Id: I74988d9d78f7074e331ef46cc0aee52c3201ab8a
diff --git a/Android.bp b/Android.bp
index 69c241d..d42fc0d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -104,3 +104,21 @@
         "-Werror",
     ],
 }
+
+// Build GKI APEX 5.4-android12-0 from $(PRODUCT_OUT)/boot.img.
+// Also generate test packages.
+gki_apex {
+    name: "com.android.gki.kmi_5_4_android12_0",
+    installable: true,
+    kmi_version: "5.4-android12-0",
+    product_out_path: "boot.img",
+    gen_test: true,
+}
+
+// Build GKI APEX 5.4-android12-0 from $(PRODUCT_OUT)/boot-5.4.img
+gki_apex {
+    name: "com.android.gki.kmi_5_4_android12_0_boot-5.4",
+    installable: false,
+    kmi_version: "5.4-android12-0",
+    product_out_path: "boot-5.4.img",
+}
diff --git a/build/Android.bp b/build/Android.bp
new file mode 100644
index 0000000..31947b2
--- /dev/null
+++ b/build/Android.bp
@@ -0,0 +1,36 @@
+// Copyright (C) 2020 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.
+
+bootstrap_go_package {
+    name: "gki-soong-rules",
+    pkgPath: "android/soong/gki",
+    pluginFor: ["soong_build"],
+    deps: [
+        "blueprint",
+        "blueprint-proptools",
+        "soong",
+        "soong-android",
+        "soong-apex",
+        "soong-phony",
+    ],
+    srcs: [
+        "gki.go",
+        "kmi.go",
+        "properties.go",
+        "raw_img_ota.go",
+    ],
+    testSrcs: [
+        "kmi_test.go",
+    ],
+}
diff --git a/build/gki.go b/build/gki.go
new file mode 100644
index 0000000..f48693b
--- /dev/null
+++ b/build/gki.go
@@ -0,0 +1,283 @@
+// Copyright (C) 2020 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 gki
+
+import (
+	"path/filepath"
+	"strings"
+
+	"android/soong/android"
+	"android/soong/apex"
+	"android/soong/etc"
+	"android/soong/genrule"
+
+	"github.com/google/blueprint/proptools"
+)
+
+type gkiApexProperties struct {
+	// Path relative to $(PRODUCT_OUT) that points to the boot image. This is
+	// passed to the generated makefile_goal.
+	// Exactly one of [factory, product_out_path] must be set.
+	Product_out_path *string
+
+	// Declared KMI version of the boot image. Example: "5.4-android12-0"
+	Kmi_version *string
+
+	// The certificate to sign the OTA payload.
+	// The name of a certificate in the default certificate directory, blank to
+	// use the default product certificate,
+	// or an android_app_certificate module name in the form ":module".
+	Ota_payload_certificate *string
+
+	// Whether test APEXes are generated. Test APEXes are named with
+	// ${name}_test_high and ${name}_test_low, respectively.
+	Gen_test *bool
+
+	// Whether this APEX is installable to one of the partitions. Default:
+	// see apex.installable.
+	Installable *bool
+
+	// Whether modules should be enabled according to board variables.
+	ModulesEnabled bool `blueprint:"mutated"`
+	// APEX package name that will be declared in the APEX manifest.
+	// e.g. com.android.gki.kmi_5_4_android12_0
+	ApexName *string `blueprint:"mutated"`
+}
+
+type gkiApex struct {
+	android.ModuleBase
+	properties gkiApexProperties
+}
+
+func init() {
+	android.RegisterModuleType("gki_apex", gkiApexFactory)
+}
+
+// Declare a GKI APEX. Generate a set of modules to define an apex with name
+// "com.android.gki" + sanitized(kmi_version).
+func gkiApexFactory() android.Module {
+	g := &gkiApex{}
+	g.AddProperties(&g.properties)
+	android.InitAndroidModule(g)
+	android.AddLoadHook(g, func(ctx android.LoadHookContext) { gkiApexMutator(ctx, g) })
+	return g
+}
+
+func gkiApexMutator(mctx android.LoadHookContext, g *gkiApex) {
+	g.validateAndSetMutableProperties(mctx)
+	g.createModulesRealApexes(mctx)
+}
+
+func (g *gkiApex) validateAndSetMutableProperties(mctx android.LoadHookContext) {
+	// Parse kmi_version property to find APEX name.
+	apexName, err := kmiVersionToApexName(proptools.String(g.properties.Kmi_version))
+	if err != nil {
+		mctx.PropertyErrorf("kmi_version", err.Error())
+		return
+	}
+
+	// Set mutable properties.
+	g.properties.ModulesEnabled = g.bootImgHasRules(mctx) && g.boardDefinesKmiVersion(mctx)
+	g.properties.ApexName = proptools.StringPtr(apexName)
+}
+
+// Create modules for a real APEX package that contains an OTA payload.
+func (g *gkiApex) createModulesRealApexes(mctx android.LoadHookContext) {
+	// Import $(PRODUCT_OUT)/boot.img to Soong
+	bootImage := g.moduleName() + "_bootimage"
+	mctx.CreateModule(android.MakefileGoalFactory, &moduleCommonProperties{
+		Name:    proptools.StringPtr(bootImage),
+		Enabled: proptools.BoolPtr(g.properties.ModulesEnabled),
+	}, &makefileGoalProperties{
+		Product_out_path: g.properties.Product_out_path,
+	})
+	// boot.img -> kernel_release.txt
+	mctx.CreateModule(genrule.GenRuleFactory, &moduleCommonProperties{
+		Name:    proptools.StringPtr(g.kernelReleaseFileName()),
+		Enabled: proptools.BoolPtr(g.properties.ModulesEnabled),
+	}, &genRuleProperties{
+		Defaults: []string{"extract_kernel_release_defaults"},
+		Srcs:     []string{":" + bootImage},
+	})
+	// boot.img -> payload.bin and payload_properties.txt
+	otaPayloadGen := g.moduleName() + "_ota_payload_gen"
+	mctx.CreateModule(rawImageOtaFactory, &moduleCommonProperties{
+		Name:    proptools.StringPtr(otaPayloadGen),
+		Enabled: proptools.BoolPtr(g.properties.ModulesEnabled),
+	}, &rawImageOtaProperties{
+		Certificate: g.properties.Ota_payload_certificate,
+		Image_goals: []string{"boot:" + bootImage},
+	})
+	// copy payload.bin to <apex>/etc/ota
+	mctx.CreateModule(etc.PrebuiltEtcFactory, &moduleCommonProperties{
+		Name:    proptools.StringPtr(g.otaPayloadName()),
+		Enabled: proptools.BoolPtr(g.properties.ModulesEnabled),
+	}, &prebuiltEtcProperties{
+		Src:                   proptools.StringPtr(":" + otaPayloadGen + "{" + payloadTag + "}"),
+		Filename_from_src:     proptools.BoolPtr(true),
+		Relative_install_path: proptools.StringPtr("ota"),
+		Installable:           proptools.BoolPtr(false),
+	})
+	// copy payload_properties.txt to <apex>/etc/ota
+	mctx.CreateModule(etc.PrebuiltEtcFactory, &moduleCommonProperties{
+		Name:    proptools.StringPtr(g.otaPropertiesName()),
+		Enabled: proptools.BoolPtr(g.properties.ModulesEnabled),
+	}, &prebuiltEtcProperties{
+		Src:                   proptools.StringPtr(":" + otaPayloadGen + "{" + payloadPropertiesTag + "}"),
+		Filename_from_src:     proptools.BoolPtr(true),
+		Relative_install_path: proptools.StringPtr("ota"),
+		Installable:           proptools.BoolPtr(false),
+	})
+	// Create the APEX module with name g.moduleName(). Use factory APEX version.
+	g.createModulesRealApex(mctx, g.moduleName(), g.properties.Installable, "")
+
+	// Create test APEX modules if gen_test. Test packages are not installable.
+	// Use hard-coded APEX version.
+	if proptools.Bool(g.properties.Gen_test) {
+		g.createModulesRealApex(mctx, g.moduleName()+"_test_high", proptools.BoolPtr(false), "1000000000")
+		g.createModulesRealApex(mctx, g.moduleName()+"_test_low", proptools.BoolPtr(false), "1")
+	}
+}
+
+func (g *gkiApex) createModulesRealApex(mctx android.LoadHookContext,
+	moduleName string,
+	overrideInstallable *bool,
+	overrideApexVersion string) {
+	// Check kmi_version property against kernel_release.txt, then
+	// kernel_release.txt -> apex_manifest.json.
+	apexManifest := moduleName + "_apex_manifest"
+	mctx.CreateModule(genrule.GenRuleFactory, &moduleCommonProperties{
+		Name:    proptools.StringPtr(apexManifest),
+		Enabled: proptools.BoolPtr(g.properties.ModulesEnabled),
+	}, &genRuleProperties{
+		Tools: []string{"build_gki_apex_manifest"},
+		Out:   []string{"apex_manifest.json"},
+		Srcs:  []string{":" + g.kernelReleaseFileName()},
+		Cmd:   proptools.StringPtr(g.createApexManifestCmd(overrideApexVersion)),
+	})
+	// The APEX module.
+	mctx.CreateModule(apex.BundleFactory, &moduleCommonProperties{
+		Name:    proptools.StringPtr(moduleName),
+		Enabled: proptools.BoolPtr(g.properties.ModulesEnabled),
+	}, &apexProperties{
+		Apex_name: g.properties.ApexName,
+		Manifest:  proptools.StringPtr(":" + apexManifest),
+		Defaults:  []string{"com.android.gki_defaults"},
+		// A real GKI APEX cannot be preinstalled to the device.
+		// It can only be provided as an update.
+		Installable: overrideInstallable,
+		Prebuilts: []string{
+			g.otaPayloadName(),
+			g.otaPropertiesName(),
+		},
+	})
+}
+
+// Original module name as specified by the "name" property.
+// This is also the APEX module name, i.e. the file name of the APEX file.
+// This is also the prefix of names of all generated modules that the phony module depends on.
+// e.g. com.android.gki.kmi_5_4_android12_0_boot
+func (g *gkiApex) moduleName() string {
+	return g.BaseModuleName()
+}
+
+// The appeared name of this gkiApex object. Exposed to Soong to avoid conflicting with
+// the generated APEX module with name moduleName().
+// e.g. com.android.gki.kmi_5_4_android12_0_boot_all
+func (g *gkiApex) Name() string {
+	return g.moduleName() + "_all"
+}
+
+// Names for intermediate modules.
+func (g *gkiApex) kernelReleaseFileName() string {
+	return g.moduleName() + "_bootimage_kernel_release_file"
+}
+
+func (g *gkiApex) otaPayloadName() string {
+	return g.moduleName() + "_ota_payload"
+}
+
+func (g *gkiApex) otaPropertiesName() string {
+	return g.moduleName() + "_ota_payload_properties"
+}
+
+// If the boot image pointed at product_out_path has no rule to be generated, do not generate any
+// build rules for this gki_apex module. For example, if this gki_apex module is:
+//     { name: "foo", product_out_path: "boot-bar.img" }
+// But there is no rule to generate boot-bar.img, then
+// - `m foo` fails with `unknown target 'foo'`
+// - checkbuild is still successful. The module foo doesn't even exist, so there
+//   is no dependency on boot-bar.img
+//
+// There is a rule to generate "boot-foo.img" if "kernel-foo" is in BOARD_KERNEL_BINARIES.
+// As a special case, there is a rule to generate "boot.img" if BOARD_KERNEL_BINARIES is empty,
+// or "kernel" is in BOARD_KERNEL_BINARIES.
+func (g *gkiApex) bootImgHasRules(mctx android.EarlyModuleContext) bool {
+	kernelNames := mctx.DeviceConfig().BoardKernelBinaries()
+	if len(kernelNames) == 0 {
+		return proptools.String(g.properties.Product_out_path) == "boot.img"
+	}
+	for _, kernelName := range kernelNames {
+		validBootImagePath := strings.Replace(kernelName, "kernel", "boot", -1) + ".img"
+		if proptools.String(g.properties.Product_out_path) == validBootImagePath {
+			return true
+		}
+	}
+	return false
+}
+
+// Only generate if this module's kmi_version property is in BOARD_KERNEL_MODULE_INTERFACE_VERSIONS.
+// Otherwise, this board does not support GKI APEXes, so no modules are generated at all.
+// This function also avoids building invalid modules in checkbuild. For example, if these
+// gki_apex modules are defined:
+//   gki_apex { name: "boot-kmi-1", kmi_version: "1", product_out_path: "boot.img" }
+//   gki_apex { name: "boot-kmi-2", kmi_version: "2", product_out_path: "boot.img" }
+// But a given device's $PRODUCT_OUT/boot.img can only support at most one KMI version.
+// Disable some modules accordingly to make sure checkbuild still works.
+func (g *gkiApex) boardDefinesKmiVersion(mctx android.EarlyModuleContext) bool {
+	kmiVersions := mctx.DeviceConfig().BoardKernelModuleInterfaceVersions()
+	return android.InList(proptools.String(g.properties.Kmi_version), kmiVersions)
+}
+
+// - For factory GKI APEX, write APEX manifest JSON to $(out) for factory APEX.
+//   e.g. 5.4-android12-0 => name: "com.android.gki.kmi_5_4_android12_0", version: factory version.
+//
+// - For real GKI APEX, transform kernel release file in $(in) to KMI version + sublevel.
+//   e.g. 5.4.42-android12-0 => name: "com.android.gki.kmi_5_4_android12_0", version: "42"
+//
+// Finally, write APEX manifest JSON to $(out).
+func (g *gkiApex) createApexManifestCmd(apexVersion string) string {
+	ret := `$(location build_gki_apex_manifest) ` +
+		`--kmi_version "` + proptools.String(g.properties.Kmi_version) + `" ` +
+		`--apex_manifest $(out) --kernel_release_file $(in)`
+	// Override version field if set.
+	if apexVersion != "" {
+		ret += ` --apex_version ` + apexVersion
+	}
+	return ret
+}
+
+func (g *gkiApex) DepsMutator(ctx android.BottomUpMutatorContext) {
+}
+
+func (g *gkiApex) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+}
+
+// OTA payload binary is signed with default_system_dev_certificate, which is equivalent to
+// DefaultAppCertificate().
+func getDefaultCertificate(ctx android.EarlyModuleContext) string {
+	pem, _ := ctx.Config().DefaultAppCertificate(ctx)
+	return strings.TrimSuffix(pem.String(), filepath.Ext(pem.String()))
+}
diff --git a/build/kmi.go b/build/kmi.go
new file mode 100644
index 0000000..27b00bf
--- /dev/null
+++ b/build/kmi.go
@@ -0,0 +1,45 @@
+// Copyright (C) 2020 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.
+
+// Minimum support of KMI version in Go. Keep in sync with libkver.
+
+package gki
+
+import (
+	"fmt"
+	"regexp"
+)
+
+var digits = "([0-9]+)"
+var reKmi = regexp.MustCompile("^([0-9]+)[.]([0-9]+)-(android[0-9]+)-([0-9]+)$")
+
+// Input is a valid KMI version, e.g. 5.4-android12-0.
+// Return a sanitized string to be used as a suffix of APEX package name
+// com.android.gki.kmi_5_4_android12_0
+// Keep in sync with libkver.
+func kmiVersionToApexName(s string) (string, error) {
+	matches := reKmi.FindAllStringSubmatch(s, 4)
+
+	if matches == nil {
+		return "", fmt.Errorf("Poorly formed KMI version: %q must match regex %q", s, reKmi)
+	}
+
+	version := matches[0][1]
+	patchLevel := matches[0][2]
+	androidRelease := matches[0][3]
+	kmiGeneration := matches[0][4]
+
+	return fmt.Sprintf("com.android.gki.kmi_%s_%s_%s_%s",
+		version, patchLevel, androidRelease, kmiGeneration), nil
+}
diff --git a/build/kmi_test.go b/build/kmi_test.go
new file mode 100644
index 0000000..de7642b
--- /dev/null
+++ b/build/kmi_test.go
@@ -0,0 +1,48 @@
+// Copyright (C) 2020 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.
+
+// Minimum support of KMI version in Go. Keep in sync with libkver.
+
+package gki
+
+import (
+	"testing"
+)
+
+func expectValid(t *testing.T, kmi string, expectedApexName string) {
+	t.Helper()
+	got, e := kmiVersionToApexName(kmi)
+	if e != nil {
+		t.Errorf("Expected no error when parsing %q, got %q", kmi, e)
+	}
+	if got != expectedApexName {
+		t.Errorf("Expected kmiVersionToApexName(%q) == %q, got %q", kmi, expectedApexName, got)
+	}
+}
+
+func expectInvalid(t *testing.T, kmi string) {
+	t.Helper()
+	got, e := kmiVersionToApexName(kmi)
+	if e == nil {
+		t.Errorf("Expected error when parsing %q, got no error with result %q", kmi, got)
+	}
+}
+
+func TestParse(t *testing.T) {
+	expectInvalid(t, "")
+	expectInvalid(t, "foobar")
+	expectInvalid(t, "1")
+	expectValid(t, "5.4-android12-0", "com.android.gki.kmi_5_4_android12_0")
+	expectValid(t, "5.4-android12-42", "com.android.gki.kmi_5_4_android12_42")
+}
diff --git a/build/properties.go b/build/properties.go
new file mode 100644
index 0000000..c6b819d
--- /dev/null
+++ b/build/properties.go
@@ -0,0 +1,49 @@
+// Copyright (C) 2020 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 gki
+
+type moduleCommonProperties struct {
+	Name     *string
+	Enabled  *bool
+	Required []string
+}
+
+type makefileGoalProperties struct {
+	Product_out_path *string
+}
+
+type genRuleProperties struct {
+	Cmd      *string
+	Defaults []string
+	Out      []string
+	Srcs     []string
+	Tools    []string
+}
+
+type prebuiltEtcProperties struct {
+	Src                   *string
+	Filename_from_src     *bool
+	Relative_install_path *string
+	Installable           *bool
+}
+
+type apexProperties struct {
+	Apex_name   *string
+	Manifest    *string
+	Defaults    []string
+	Installable *bool
+	Prebuilts   []string
+	Overrides   []string
+}
diff --git a/build/raw_img_ota.go b/build/raw_img_ota.go
new file mode 100644
index 0000000..2e528d4
--- /dev/null
+++ b/build/raw_img_ota.go
@@ -0,0 +1,244 @@
+// Copyright (C) 2020 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.
+
+// A special genrule that creates OTA payload and payload_properties from a raw
+// image. This rule is created so that the two outputs, payload and
+// payload_properties, can be distinguished with tags.
+
+package gki
+
+import (
+	"fmt"
+	"strings"
+
+	"android/soong/android"
+	"android/soong/java"
+
+	"github.com/google/blueprint"
+	"github.com/google/blueprint/proptools"
+)
+
+type dependencyTag struct {
+	blueprint.BaseDependencyTag
+	name string
+}
+
+// ["foo", "bar"] -> ["${foo}", "${bar}"]
+func toVars(deps []string) []string {
+	ret := []string{}
+	for _, dep := range deps {
+		ret = append(ret, fmt.Sprintf("${%s}", dep))
+	}
+	return ret
+}
+
+var (
+	certificateTag = dependencyTag{name: "certificate"}
+	rawImageTag    = dependencyTag{name: "raw_image"}
+
+	pctx = android.NewPackageContext("android/gki")
+
+	otaFromRawImageDeps = []string{
+		"ota_from_raw_image",
+
+		// Needed by ota_from_target_files
+		"brillo_update_payload",
+
+		// Needed by brillo_update_payload
+		"delta_generator",
+
+		// Needed by GetBootImageTimestamp
+		"lz4",
+		"toybox",
+		"unpack_bootimg",
+	}
+
+	otaFromRawImageVarDeps = toVars(otaFromRawImageDeps)
+
+	otaFromRawImageRule = pctx.AndroidStaticRule("ota_from_raw_image", blueprint.RuleParams{
+		Command: `${ota_from_raw_image} --tools ` + strings.Join(otaFromRawImageVarDeps, " ") +
+			` ${kwargs} --out ${outDir} -- ${inputArg}`,
+		CommandDeps: otaFromRawImageVarDeps,
+		Description: "ota_from_raw_image ${outDir}",
+	}, "kwargs", "outDir", "inputArg")
+
+	// Tags to OutputFiles
+	payloadTag           = "payload"
+	payloadPropertiesTag = "properties"
+)
+
+func init() {
+	for _, dep := range otaFromRawImageDeps {
+		pctx.HostBinToolVariable(dep, dep)
+	}
+	// Intentionally not register this module so that it can only be constructed by gki_apex.
+}
+
+type rawImageOtaProperties struct {
+	// The name of a certificate in the default certificate directory, blank to use the default product certificate,
+	// or an android_app_certificate module name in the form ":module".
+	Certificate *string
+
+	// A set of images and their related modules. Must be in this form
+	// IMAGE_NAME:MODULE, where IMAGE_NAME is an image name like "boot", and
+	// MODULE is the name of a makefile_goal.
+	Image_goals []string
+}
+
+type rawImageOta struct {
+	android.ModuleBase
+	properties rawImageOtaProperties
+
+	pem android.Path
+	key android.Path
+
+	outPayload    android.WritablePath
+	outProperties android.WritablePath
+}
+
+// Declare a rule that generates a signed OTA payload from a raw image. This
+// includes payload.bin and payload_properties.txt.
+func rawImageOtaFactory() android.Module {
+	r := &rawImageOta{}
+	r.AddProperties(&r.properties)
+	android.InitAndroidModule(r)
+	return r
+}
+
+func (r *rawImageOta) OutputFiles(tag string) (android.Paths, error) {
+	switch tag {
+	case "":
+		return android.Paths{r.outPayload, r.outProperties}, nil
+	case payloadTag:
+		return android.Paths{r.outPayload}, nil
+	case payloadPropertiesTag:
+		return android.Paths{r.outProperties}, nil
+	default:
+		return nil, fmt.Errorf("unsupported module reference tag %q", tag)
+	}
+}
+
+var _ android.OutputFileProducer = (*rawImageOta)(nil)
+
+func (r *rawImageOta) getCertString(ctx android.BaseModuleContext) string {
+	moduleName := ctx.ModuleName()
+	certificate, overridden := ctx.DeviceConfig().OverrideCertificateFor(moduleName)
+	if overridden {
+		return ":" + certificate
+	}
+	return proptools.String(r.properties.Certificate)
+}
+
+// Returns module->image_name mapping, e.g. "bootimage_soong"->"boot"
+func (r *rawImageOta) goalToImage(ctx android.EarlyModuleContext) map[string]string {
+	ret := map[string]string{}
+	for _, imageGoal := range r.properties.Image_goals {
+		lst := strings.Split(imageGoal, ":")
+		if len(lst) != 2 {
+			ctx.PropertyErrorf("image_goals", "Must be in the form IMAGE_NAME:MODULE")
+			return map[string]string{}
+		}
+		ret[lst[1]] = lst[0]
+	}
+	return ret
+}
+
+func (r *rawImageOta) DepsMutator(ctx android.BottomUpMutatorContext) {
+	// Add dependency to modules in image_goals
+	for module, _ := range r.goalToImage(ctx) {
+		ctx.AddVariationDependencies(nil, rawImageTag, module)
+	}
+	// Add dependency to certificate module, if any.
+	cert := android.SrcIsModule(r.getCertString(ctx))
+	if cert != "" {
+		ctx.AddVariationDependencies(nil, certificateTag, cert)
+	}
+}
+
+func (r *rawImageOta) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+	inputArg := []string{}
+	kwargs := []string{}
+	implicits := android.Paths{}
+
+	// Handle image_goals
+	goalToImage := r.goalToImage(ctx)
+	ctx.VisitDirectDepsWithTag(rawImageTag, func(module android.Module) {
+		depName := ctx.OtherModuleName(module)
+		imgPath := android.OutputFileForModule(ctx, module, "")
+		if imgPath != nil {
+			implicits = append(implicits, imgPath)
+			inputArg = append(inputArg, goalToImage[depName]+":"+imgPath.String())
+		} else {
+			ctx.ModuleErrorf("image dependency %q does not generate any output", depName)
+		}
+	})
+
+	// Handle certificate
+	ctx.VisitDirectDepsWithTag(certificateTag, func(module android.Module) {
+		depName := ctx.OtherModuleName(module)
+		if cert, ok := module.(*java.AndroidAppCertificate); ok {
+			r.pem = cert.Certificate.Pem
+			r.key = cert.Certificate.Key
+		} else {
+			ctx.ModuleErrorf("certificate dependency %q must be an android_app_certificate module", depName)
+		}
+	})
+	r.setCertificateAndPrivateKey(ctx)
+	keyName, keyError := removeCertExt(r.pem)
+	if keyError != nil {
+		ctx.ModuleErrorf("Cannot get certificate to sign the OTA payload binary: " + keyError.Error())
+	}
+	implicits = append(implicits, r.pem, r.key)
+	kwargs = append(kwargs, "--key "+proptools.String(keyName))
+
+	// Set outputs
+	outDir := android.PathForModuleGen(ctx, "payload_files")
+	r.outPayload = outDir.Join(ctx, "payload.bin")
+	r.outProperties = outDir.Join(ctx, "payload_properties.txt")
+
+	ctx.Build(pctx, android.BuildParams{
+		Rule:        otaFromRawImageRule,
+		Description: "Generate OTA from raw image",
+		Implicits:   implicits,
+		Outputs:     android.WritablePaths{r.outPayload, r.outProperties},
+		Args: map[string]string{
+			"kwargs":   strings.Join(kwargs, " "),
+			"outDir":   outDir.String(),
+			"inputArg": strings.Join(inputArg, " "),
+		},
+	})
+}
+
+func (r *rawImageOta) setCertificateAndPrivateKey(ctx android.ModuleContext) {
+	if r.pem == nil {
+		cert := proptools.String(r.properties.Certificate)
+		if cert == "" {
+			pem, key := ctx.Config().DefaultAppCertificate(ctx)
+			r.pem = pem
+			r.key = key
+		} else {
+			defaultDir := ctx.Config().DefaultAppCertificateDir(ctx)
+			r.pem = defaultDir.Join(ctx, cert+".x509.pem")
+			r.key = defaultDir.Join(ctx, cert+".pk8")
+		}
+	}
+}
+
+func removeCertExt(path android.Path) (*string, error) {
+	s := path.String()
+	if strings.HasSuffix(s, ".x509.pem") {
+		return proptools.StringPtr(strings.TrimSuffix(s, ".x509.pem")), nil
+	}
+	return nil, fmt.Errorf("Path %q does not end with .x509.pem", s)
+}