Add module based host-tools snapshot

Add new module:
host_snapshot {
    name: "host-snapshot"
    deps: [
      (list of host tools)
    ],
    ...
}

Package host tools using android.PackagingBase to capture host tools and transitive packaging data.
Add JSON meta data to snapshot that allows snapshot to be installed via development/vendor_snapshot/update.py

Add support to generate a fake host snapshot of all host modules that is used to detect required
modules via development/vendor_snapshot/update.py.

Bug: 192556798
Bug: 194799048
Bug: 192896149
Test: m HOST_FAKE_SNAPSHOT_ENABLE=true host-fake-snapshot dist -- check snapshot exists in dist

Change-Id: I849c4db801cd858408f6fe6a3ce69262a23a5be9
diff --git a/android/config.go b/android/config.go
index b3b8f3c..fb16db1 100644
--- a/android/config.go
+++ b/android/config.go
@@ -1517,6 +1517,10 @@
 		c.config.productVariables.RecoverySnapshotDirsIncluded)
 }
 
+func (c *deviceConfig) HostFakeSnapshotEnabled() bool {
+	return c.config.productVariables.HostFakeSnapshotEnabled
+}
+
 func (c *deviceConfig) ShippingApiLevel() ApiLevel {
 	if c.config.productVariables.ShippingApiLevel == nil {
 		return NoneApiLevel
diff --git a/android/defs.go b/android/defs.go
index b3ff376..c8e2e9b 100644
--- a/android/defs.go
+++ b/android/defs.go
@@ -188,6 +188,15 @@
 	buildWriteFileRule(ctx, outputFile, content)
 }
 
+func CatFileRule(ctx BuilderContext, paths Paths, outputFile WritablePath) {
+	ctx.Build(pctx, BuildParams{
+		Rule:        Cat,
+		Inputs:      paths,
+		Output:      outputFile,
+		Description: "combine files to " + outputFile.Base(),
+	})
+}
+
 // shellUnescape reverses proptools.ShellEscape
 func shellUnescape(s string) string {
 	// Remove leading and trailing quotes if present
diff --git a/android/variable.go b/android/variable.go
index 0fb9078..51e90ce 100644
--- a/android/variable.go
+++ b/android/variable.go
@@ -321,6 +321,7 @@
 	VendorSnapshotDirsExcluded   []string `json:",omitempty"`
 	RecoverySnapshotDirsExcluded []string `json:",omitempty"`
 	RecoverySnapshotDirsIncluded []string `json:",omitempty"`
+	HostFakeSnapshotEnabled      bool     `json:",omitempty"`
 
 	BoardVendorSepolicyDirs      []string `json:",omitempty"`
 	BoardOdmSepolicyDirs         []string `json:",omitempty"`
diff --git a/cc/cc.go b/cc/cc.go
index 39d89e5..34d8b6f 100644
--- a/cc/cc.go
+++ b/cc/cc.go
@@ -30,6 +30,7 @@
 	"android/soong/android"
 	"android/soong/cc/config"
 	"android/soong/genrule"
+	"android/soong/snapshot"
 )
 
 func init() {
@@ -3386,6 +3387,8 @@
 	return c.IsStubs() || c.Target().NativeBridge == android.NativeBridgeEnabled
 }
 
+var _ snapshot.RelativeInstallPath = (*Module)(nil)
+
 //
 // Defaults
 //
diff --git a/cc/vendor_snapshot.go b/cc/vendor_snapshot.go
index ba4d79f..8a17e2e 100644
--- a/cc/vendor_snapshot.go
+++ b/cc/vendor_snapshot.go
@@ -132,12 +132,9 @@
 	return false
 }
 
-// This is to be saved as .json files, which is for development/vendor_snapshot/update.py.
-// These flags become Android.bp snapshot module properties.
+// Extend the snapshot.SnapshotJsonFlags to include cc specific fields.
 type snapshotJsonFlags struct {
-	ModuleName          string `json:",omitempty"`
-	RelativeInstallPath string `json:",omitempty"`
-
+	snapshot.SnapshotJsonFlags
 	// library flags
 	ExportedDirs       []string `json:",omitempty"`
 	ExportedSystemDirs []string `json:",omitempty"`
@@ -154,7 +151,6 @@
 	SharedLibs  []string `json:",omitempty"`
 	StaticLibs  []string `json:",omitempty"`
 	RuntimeLibs []string `json:",omitempty"`
-	Required    []string `json:",omitempty"`
 
 	// extra config files
 	InitRc         []string `json:",omitempty"`
diff --git a/etc/prebuilt_etc.go b/etc/prebuilt_etc.go
index 8aeb0dd..5979ca7 100644
--- a/etc/prebuilt_etc.go
+++ b/etc/prebuilt_etc.go
@@ -519,13 +519,6 @@
 	return module
 }
 
-// Flags to be included in the snapshot
-type snapshotJsonFlags struct {
-	ModuleName          string `json:",omitempty"`
-	Filename            string `json:",omitempty"`
-	RelativeInstallPath string `json:",omitempty"`
-}
-
 // Copy file into the snapshot
 func copyFile(ctx android.SingletonContext, path android.Path, out string, fake bool) android.OutputPath {
 	if fake {
@@ -612,7 +605,7 @@
 		snapshotLibOut := filepath.Join(snapshotArchDir, targetArch, "etc", m.BaseModuleName())
 		snapshotOutputs = append(snapshotOutputs, copyFile(ctx, m.OutputFile(), snapshotLibOut, s.Fake))
 
-		prop := snapshotJsonFlags{}
+		prop := snapshot.SnapshotJsonFlags{}
 		propOut := snapshotLibOut + ".json"
 		prop.ModuleName = m.BaseModuleName()
 		if m.subdirProperties.Relative_install_path != nil {
diff --git a/python/python.go b/python/python.go
index 0f5b788..2a848ca 100644
--- a/python/python.go
+++ b/python/python.go
@@ -310,13 +310,16 @@
 // HostToolPath returns a path if appropriate such that this module can be used as a host tool,
 // fulfilling HostToolProvider interface.
 func (p *Module) HostToolPath() android.OptionalPath {
-	if p.installer == nil {
-		// python_library is just meta module, and doesn't have any installer.
-		return android.OptionalPath{}
+	if p.installer != nil {
+		if bin, ok := p.installer.(*binaryDecorator); ok {
+			// TODO: This should only be set when building host binaries -- tests built for device would be
+			// setting this incorrectly.
+			return android.OptionalPathForPath(bin.path)
+		}
 	}
-	// TODO: This should only be set when building host binaries -- tests built for device would be
-	// setting this incorrectly.
-	return android.OptionalPathForPath(p.installer.(*binaryDecorator).path)
+
+	return android.OptionalPath{}
+
 }
 
 // OutputFiles returns output files based on given tag, returns an error if tag is unsupported.
diff --git a/sh/sh_binary.go b/sh/sh_binary.go
index 1647428..b1f77d0 100644
--- a/sh/sh_binary.go
+++ b/sh/sh_binary.go
@@ -26,6 +26,7 @@
 	"android/soong/android"
 	"android/soong/bazel"
 	"android/soong/cc"
+	"android/soong/snapshot"
 	"android/soong/tradefed"
 )
 
@@ -195,6 +196,9 @@
 	return proptools.String(s.properties.Sub_dir)
 }
 
+func (s *ShBinary) RelativeInstallPath() string {
+	return s.SubDir()
+}
 func (s *ShBinary) Installable() bool {
 	return s.properties.Installable == nil || proptools.Bool(s.properties.Installable)
 }
@@ -566,3 +570,5 @@
 func (m *bazelShBinary) GenerateAndroidBuildActions(ctx android.ModuleContext) {}
 
 var Bool = proptools.Bool
+
+var _ snapshot.RelativeInstallPath = (*ShBinary)(nil)
diff --git a/snapshot/Android.bp b/snapshot/Android.bp
index f17ac53..3354993 100644
--- a/snapshot/Android.bp
+++ b/snapshot/Android.bp
@@ -11,12 +11,20 @@
         "soong",
         "soong-android",
     ],
+    // Source file name convention is to include _snapshot as a
+    // file suffix for files that are generating snapshots.
     srcs: [
+        "host_fake_snapshot.go",
+        "host_snapshot.go",
         "recovery_snapshot.go",
         "snapshot.go",
         "snapshot_base.go",
         "util.go",
         "vendor_snapshot.go",
     ],
+    testSrcs: [
+        "host_test.go",
+        "test.go",
+    ],
     pluginFor: ["soong_build"],
 }
diff --git a/snapshot/host_fake_snapshot.go b/snapshot/host_fake_snapshot.go
new file mode 100644
index 0000000..6b4e12b
--- /dev/null
+++ b/snapshot/host_fake_snapshot.go
@@ -0,0 +1,149 @@
+// Copyright 2021 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 snapshot
+
+import (
+	"encoding/json"
+	"path/filepath"
+
+	"android/soong/android"
+)
+
+// The host_snapshot module creates a snapshot of host tools to be used
+// in a minimal source tree.   In order to create the host_snapshot the
+// user must explicitly list the modules to be included.  The
+// host-fake-snapshot, defined in this file, is a utility to help determine
+// which host modules are being used in the minimal source tree.
+//
+// The host-fake-snapshot is designed to run in a full source tree and
+// will result in a snapshot that contains an empty file for each host
+// tool found in the tree.  The fake snapshot is only used to determine
+// the host modules that the minimal source tree depends on, hence the
+// snapshot uses an empty file for each module and saves on having to
+// actually build any tool to generate the snapshot.  The fake snapshot
+// is compatible with an actual host_snapshot and is installed into a
+// minimal source tree via the development/vendor_snapshot/update.py
+// script.
+//
+// After generating the fake snapshot and installing into the minimal
+// source tree, the dependent modules are determined via the
+// development/vendor_snapshot/update.py script (see script for more
+// information).  These modules are then used to define the actual
+// host_snapshot to be used.  This is a similar process to the other
+// snapshots (vendor, recovery,...)
+//
+// Example
+//
+// Full source tree:
+//   1/ Generate fake host snapshot
+//
+// Minimal source tree:
+//   2/ Install the fake host snapshot
+//   3/ List the host modules used from the snapshot
+//   4/ Remove fake host snapshot
+//
+// Full source tree:
+//   4/ Create host_snapshot with modules identified in step 3
+//
+// Minimal source tree:
+//   5/ Install host snapshot
+//   6/ Build
+//
+// The host-fake-snapshot is a singleton module, that will be built
+// if HOST_FAKE_SNAPSHOT_ENABLE=true.
+
+func init() {
+	registerHostSnapshotComponents(android.InitRegistrationContext)
+}
+
+func registerHostSnapshotComponents(ctx android.RegistrationContext) {
+	ctx.RegisterSingletonType("host-fake-snapshot", HostToolsFakeAndroidSingleton)
+}
+
+type hostFakeSingleton struct {
+	snapshotDir string
+	zipFile     android.OptionalPath
+}
+
+func (c *hostFakeSingleton) init() {
+	c.snapshotDir = "host-fake-snapshot"
+
+}
+func HostToolsFakeAndroidSingleton() android.Singleton {
+	singleton := &hostFakeSingleton{}
+	singleton.init()
+	return singleton
+}
+
+func (c *hostFakeSingleton) GenerateBuildActions(ctx android.SingletonContext) {
+	if !ctx.DeviceConfig().HostFakeSnapshotEnabled() {
+		return
+	}
+	// Find all host binary modules add 'fake' versions to snapshot
+	var outputs android.Paths
+	seen := make(map[string]bool)
+	var jsonData []SnapshotJsonFlags
+	ctx.VisitAllModules(func(module android.Module) {
+		if module.Target().Os != ctx.Config().BuildOSTarget.Os {
+			return
+		}
+		if module.Target().Arch.ArchType != ctx.Config().BuildOSTarget.Arch.ArchType {
+			return
+		}
+
+		if android.IsModulePrebuilt(module) {
+			return
+		}
+
+		if !module.Enabled() || module.IsHideFromMake() {
+			return
+		}
+		apexInfo := ctx.ModuleProvider(module, android.ApexInfoProvider).(android.ApexInfo)
+		if !apexInfo.IsForPlatform() {
+			return
+		}
+		path := hostBinToolPath(module)
+		if path.Valid() && path.String() != "" {
+			outFile := filepath.Join(c.snapshotDir, path.String())
+			if !seen[outFile] {
+				seen[outFile] = true
+				outputs = append(outputs, WriteStringToFileRule(ctx, "", outFile))
+				jsonData = append(jsonData, *hostBinJsonDesc(module))
+			}
+		}
+	})
+
+	marsh, err := json.Marshal(jsonData)
+	if err != nil {
+		ctx.Errorf("host fake snapshot json marshal failure: %#v", err)
+		return
+	}
+	outputs = append(outputs, WriteStringToFileRule(ctx, string(marsh), filepath.Join(c.snapshotDir, "host_snapshot.json")))
+	c.zipFile = zipSnapshot(ctx, c.snapshotDir, c.snapshotDir, outputs)
+
+}
+func (c *hostFakeSingleton) MakeVars(ctx android.MakeVarsContext) {
+	if !c.zipFile.Valid() {
+		return
+	}
+	ctx.Phony(
+		"host-fake-snapshot",
+		c.zipFile.Path())
+
+	ctx.DistForGoal(
+		"host-fake-snapshot",
+		c.zipFile.Path())
+
+}
diff --git a/snapshot/host_snapshot.go b/snapshot/host_snapshot.go
new file mode 100644
index 0000000..2a25a00
--- /dev/null
+++ b/snapshot/host_snapshot.go
@@ -0,0 +1,221 @@
+// Copyright 2021 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 snapshot
+
+import (
+	"encoding/json"
+	"fmt"
+	"path/filepath"
+	"sort"
+
+	"github.com/google/blueprint"
+	"github.com/google/blueprint/proptools"
+
+	"android/soong/android"
+)
+
+//
+// The host_snapshot module creates a snapshot of the modules defined in
+// the deps property.  The modules within the deps property (host tools)
+// are ones that return a valid path via HostToolPath() of the
+// HostToolProvider.  The created snapshot contains the binaries and any
+// transitive PackagingSpecs of the included host tools, along with a JSON
+// meta file.
+//
+// The snapshot is installed into a source tree via
+// development/vendor_snapshot/update.py, the included modules are
+// provided as preferred prebuilts.
+//
+// To determine which tools to include in the host snapshot see
+// host_fake_snapshot.go.
+
+func init() {
+	registerHostBuildComponents(android.InitRegistrationContext)
+}
+
+func registerHostBuildComponents(ctx android.RegistrationContext) {
+	ctx.RegisterModuleType("host_snapshot", hostSnapshotFactory)
+}
+
+// Relative installation path
+type RelativeInstallPath interface {
+	RelativeInstallPath() string
+}
+
+type hostSnapshot struct {
+	android.ModuleBase
+	android.PackagingBase
+
+	zipFile    android.OptionalPath
+	installDir android.InstallPath
+}
+
+func hostSnapshotFactory() android.Module {
+	module := &hostSnapshot{}
+	initHostToolsModule(module)
+	return module
+}
+func initHostToolsModule(module *hostSnapshot) {
+	android.InitPackageModule(module)
+	android.InitAndroidMultiTargetsArchModule(module, android.HostSupported, android.MultilibCommon)
+}
+
+var dependencyTag = struct {
+	blueprint.BaseDependencyTag
+	android.InstallAlwaysNeededDependencyTag
+	android.PackagingItemAlwaysDepTag
+}{}
+
+func (f *hostSnapshot) DepsMutator(ctx android.BottomUpMutatorContext) {
+	f.AddDeps(ctx, dependencyTag)
+}
+func (f *hostSnapshot) installFileName() string {
+	return f.Name() + ".zip"
+}
+
+// Create zipfile with JSON description, notice files... for dependent modules
+func (f *hostSnapshot) CreateMetaData(ctx android.ModuleContext, fileName string) android.OutputPath {
+	var jsonData []SnapshotJsonFlags
+	var metaPaths android.Paths
+
+	metaZipFile := android.PathForModuleOut(ctx, fileName).OutputPath
+
+	// Create JSON file based on the direct dependencies
+	ctx.VisitDirectDeps(func(dep android.Module) {
+		desc := hostBinJsonDesc(dep)
+		if desc != nil {
+			jsonData = append(jsonData, *desc)
+		}
+		if len(dep.EffectiveLicenseFiles()) > 0 {
+			noticeFile := android.PathForModuleOut(ctx, "NOTICE_FILES", dep.Name()+".txt").OutputPath
+			android.CatFileRule(ctx, dep.EffectiveLicenseFiles(), noticeFile)
+			metaPaths = append(metaPaths, noticeFile)
+		}
+
+	})
+	// Sort notice paths and json data for repeatble build
+	sort.Slice(jsonData, func(i, j int) bool {
+		return (jsonData[i].ModuleName < jsonData[j].ModuleName)
+	})
+	sort.Slice(metaPaths, func(i, j int) bool {
+		return (metaPaths[i].String() < metaPaths[j].String())
+	})
+
+	marsh, err := json.Marshal(jsonData)
+	if err != nil {
+		ctx.ModuleErrorf("host snapshot json marshal failure: %#v", err)
+		return android.OutputPath{}
+	}
+
+	jsonZipFile := android.PathForModuleOut(ctx, "host_snapshot.json").OutputPath
+	metaPaths = append(metaPaths, jsonZipFile)
+	rspFile := android.PathForModuleOut(ctx, "host_snapshot.rsp").OutputPath
+	android.WriteFileRule(ctx, jsonZipFile, string(marsh))
+
+	builder := android.NewRuleBuilder(pctx, ctx)
+
+	builder.Command().
+		BuiltTool("soong_zip").
+		FlagWithArg("-C ", android.PathForModuleOut(ctx).OutputPath.String()).
+		FlagWithOutput("-o ", metaZipFile).
+		FlagWithRspFileInputList("-r ", rspFile, metaPaths)
+	builder.Build("zip_meta", fmt.Sprintf("zipping meta data for %s", ctx.ModuleName()))
+
+	return metaZipFile
+}
+
+// Create the host tool zip file
+func (f *hostSnapshot) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+	// Create a zip file for the binaries, and a zip of the meta data, then merge zips
+	depsZipFile := android.PathForModuleOut(ctx, f.Name()+"_deps.zip").OutputPath
+	modsZipFile := android.PathForModuleOut(ctx, f.Name()+"_mods.zip").OutputPath
+	outputFile := android.PathForModuleOut(ctx, f.installFileName()).OutputPath
+
+	f.installDir = android.PathForModuleInstall(ctx)
+
+	f.CopyDepsToZip(ctx, depsZipFile)
+
+	builder := android.NewRuleBuilder(pctx, ctx)
+	builder.Command().
+		BuiltTool("zip2zip").
+		FlagWithInput("-i ", depsZipFile).
+		FlagWithOutput("-o ", modsZipFile).
+		Text("**/*:" + proptools.ShellEscape(f.installDir.String()))
+
+	metaZipFile := f.CreateMetaData(ctx, f.Name()+"_meta.zip")
+
+	builder.Command().
+		BuiltTool("merge_zips").
+		Output(outputFile).
+		Input(metaZipFile).
+		Input(modsZipFile)
+
+	builder.Build("manifest", fmt.Sprintf("Adding manifest %s", f.installFileName()))
+	zip := ctx.InstallFile(f.installDir, f.installFileName(), outputFile)
+	f.zipFile = android.OptionalPathForPath(zip)
+
+}
+
+// Implements android.AndroidMkEntriesProvider
+func (f *hostSnapshot) AndroidMkEntries() []android.AndroidMkEntries {
+	if !f.zipFile.Valid() {
+		return []android.AndroidMkEntries{}
+	}
+
+	return []android.AndroidMkEntries{android.AndroidMkEntries{
+		Class:      "ETC",
+		OutputFile: f.zipFile,
+		DistFiles:  android.MakeDefaultDistFiles(f.zipFile.Path()),
+		ExtraEntries: []android.AndroidMkExtraEntriesFunc{
+			func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
+				entries.SetString("LOCAL_MODULE_PATH", f.installDir.ToMakePath().String())
+				entries.SetString("LOCAL_INSTALLED_MODULE_STEM", f.installFileName())
+			},
+		},
+	}}
+}
+
+// Get host tools path and relative install string helpers
+func hostBinToolPath(m android.Module) android.OptionalPath {
+	if provider, ok := m.(android.HostToolProvider); ok {
+		return provider.HostToolPath()
+	}
+	return android.OptionalPath{}
+
+}
+func hostRelativePathString(m android.Module) string {
+	var outString string
+	if rel, ok := m.(RelativeInstallPath); ok {
+		outString = rel.RelativeInstallPath()
+	}
+	return outString
+}
+
+// Create JSON description for given module, only create descriptions for binary modueles which
+// provide a valid HostToolPath
+func hostBinJsonDesc(m android.Module) *SnapshotJsonFlags {
+	path := hostBinToolPath(m)
+	relPath := hostRelativePathString(m)
+	if path.Valid() && path.String() != "" {
+		return &SnapshotJsonFlags{
+			ModuleName:          m.Name(),
+			ModuleStemName:      filepath.Base(path.String()),
+			Filename:            path.String(),
+			Required:            append(m.HostRequiredModuleNames(), m.RequiredModuleNames()...),
+			RelativeInstallPath: relPath,
+		}
+	}
+	return nil
+}
diff --git a/snapshot/host_test.go b/snapshot/host_test.go
new file mode 100644
index 0000000..ab9fedd
--- /dev/null
+++ b/snapshot/host_test.go
@@ -0,0 +1,170 @@
+// Copyright 2021 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 snapshot
+
+import (
+	"path/filepath"
+	"testing"
+
+	"android/soong/android"
+)
+
+// host_snapshot and host-fake-snapshot test functions
+
+type hostTestModule struct {
+	android.ModuleBase
+	props struct {
+		Deps []string
+	}
+}
+
+func hostTestBinOut(bin string) string {
+	return filepath.Join("out", "bin", bin)
+}
+
+func (c *hostTestModule) HostToolPath() android.OptionalPath {
+	return (android.OptionalPathForPath(android.PathForTesting(hostTestBinOut(c.Name()))))
+}
+
+func hostTestModuleFactory() android.Module {
+	m := &hostTestModule{}
+	m.AddProperties(&m.props)
+	android.InitAndroidArchModule(m, android.HostSupported, android.MultilibFirst)
+	return m
+}
+func (m *hostTestModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+	builtFile := android.PathForModuleOut(ctx, m.Name())
+	dir := ctx.Target().Arch.ArchType.Multilib
+	installDir := android.PathForModuleInstall(ctx, dir)
+	ctx.InstallFile(installDir, m.Name(), builtFile)
+}
+
+// Common blueprint used for testing
+var hostTestBp = `
+		license_kind {
+			name: "test_notice",
+			conditions: ["notice"],
+		}
+		license {
+			name: "host_test_license",
+			visibility: ["//visibility:public"],
+			license_kinds: [
+				"test_notice"
+			],
+			license_text: [
+				"NOTICE",
+			],
+		}
+		component {
+			name: "foo",
+			deps: ["bar"],
+		}
+		component {
+			name: "bar",
+			licenses: ["host_test_license"],
+		}
+		`
+
+var hostTestModBp = `
+		host_snapshot {
+			name: "test-host-snapshot",
+			deps: [
+				"foo",
+			],
+		}
+		`
+
+var prepareForHostTest = android.GroupFixturePreparers(
+	android.PrepareForTestWithAndroidBuildComponents,
+	android.PrepareForTestWithLicenses,
+	android.FixtureRegisterWithContext(func(ctx android.RegistrationContext) {
+		ctx.RegisterModuleType("component", hostTestModuleFactory)
+	}),
+)
+
+// Prepare for host_snapshot test
+var prepareForHostModTest = android.GroupFixturePreparers(
+	prepareForHostTest,
+	android.FixtureWithRootAndroidBp(hostTestBp+hostTestModBp),
+	android.FixtureRegisterWithContext(func(ctx android.RegistrationContext) {
+		registerHostBuildComponents(ctx)
+	}),
+)
+
+// Prepare for fake host snapshot test disabled
+var prepareForFakeHostTest = android.GroupFixturePreparers(
+	prepareForHostTest,
+	android.FixtureWithRootAndroidBp(hostTestBp),
+	android.FixtureRegisterWithContext(func(ctx android.RegistrationContext) {
+		registerHostSnapshotComponents(ctx)
+	}),
+)
+
+// Prepare for fake host snapshot test enabled
+var prepareForFakeHostTestEnabled = android.GroupFixturePreparers(
+	prepareForFakeHostTest,
+	android.FixtureModifyProductVariables(func(variables android.FixtureProductVariables) {
+		variables.HostFakeSnapshotEnabled = true
+	}),
+)
+
+// Validate that a hostSnapshot object is created containing zip files and JSON file
+// content of zip file is not validated as this is done by PackagingSpecs
+func TestHostSnapshot(t *testing.T) {
+	result := prepareForHostModTest.RunTest(t)
+	t.Helper()
+	ctx := result.TestContext.ModuleForTests("test-host-snapshot", result.Config.BuildOS.String()+"_common")
+	mod := ctx.Module().(*hostSnapshot)
+	if ctx.MaybeOutput("host_snapshot.json").Rule == nil {
+		t.Error("Manifest file not found")
+	}
+	zips := []string{"_deps.zip", "_mods.zip", ".zip"}
+
+	for _, zip := range zips {
+		zFile := mod.Name() + zip
+		if ctx.MaybeOutput(zFile).Rule == nil {
+			t.Error("Zip file ", zFile, "not found")
+		}
+
+	}
+}
+
+// Validate fake host snapshot contains binary modules as well as the JSON meta file
+func TestFakeHostSnapshotEnable(t *testing.T) {
+	result := prepareForFakeHostTestEnabled.RunTest(t)
+	t.Helper()
+	bins := []string{"foo", "bar"}
+	ctx := result.TestContext.SingletonForTests("host-fake-snapshot")
+	if ctx.MaybeOutput(filepath.Join("host-fake-snapshot", "host_snapshot.json")).Rule == nil {
+		t.Error("Manifest file not found")
+	}
+	for _, bin := range bins {
+		if ctx.MaybeOutput(filepath.Join("host-fake-snapshot", hostTestBinOut(bin))).Rule == nil {
+			t.Error("Binary file ", bin, "not found")
+		}
+
+	}
+}
+
+// Validate not fake host snapshot if HostFakeSnapshotEnabled has not been set to true
+func TestFakeHostSnapshotDisable(t *testing.T) {
+	result := prepareForFakeHostTest.RunTest(t)
+	t.Helper()
+	ctx := result.TestContext.SingletonForTests("host-fake-snapshot")
+	if len(ctx.AllOutputs()) != 0 {
+		t.Error("Fake host snapshot not empty when disabled")
+	}
+
+}
diff --git a/snapshot/snapshot_base.go b/snapshot/snapshot_base.go
index de93f3e..79d3cf6 100644
--- a/snapshot/snapshot_base.go
+++ b/snapshot/snapshot_base.go
@@ -102,3 +102,19 @@
 		return isDirectoryExcluded(filepath.Dir(dir), excludedMap, includedMap)
 	}
 }
+
+// This is to be saved as .json files, which is for development/vendor_snapshot/update.py.
+// These flags become Android.bp snapshot module properties.
+//
+// Attributes are optional and will be populated based on each module's need.
+// Common attributes are defined here, languages may extend this struct to add
+// additional attributes.
+type SnapshotJsonFlags struct {
+	ModuleName          string `json:",omitempty"`
+	RelativeInstallPath string `json:",omitempty"`
+	Filename            string `json:",omitempty"`
+	ModuleStemName      string `json:",omitempty"`
+
+	// dependencies
+	Required []string `json:",omitempty"`
+}
diff --git a/snapshot/test.go b/snapshot/test.go
new file mode 100644
index 0000000..346af2b
--- /dev/null
+++ b/snapshot/test.go
@@ -0,0 +1,24 @@
+// Copyright 2021 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 snapshot
+
+import (
+	"os"
+	"testing"
+)
+
+func TestMain(m *testing.M) {
+	os.Exit(m.Run())
+}
diff --git a/snapshot/util.go b/snapshot/util.go
index 2297dfc..f447052 100644
--- a/snapshot/util.go
+++ b/snapshot/util.go
@@ -34,3 +34,22 @@
 	})
 	return outPath
 }
+
+// zip snapshot
+func zipSnapshot(ctx android.SingletonContext, dir string, baseName string, snapshotOutputs android.Paths) android.OptionalPath {
+	zipPath := android.PathForOutput(
+		ctx, dir, baseName+".zip")
+
+	zipRule := android.NewRuleBuilder(pctx, ctx)
+	rspFile := android.PathForOutput(
+		ctx, dir, baseName+"_list.rsp")
+
+	zipRule.Command().
+		BuiltTool("soong_zip").
+		FlagWithOutput("-o ", zipPath).
+		FlagWithArg("-C ", android.PathForOutput(ctx, dir).String()).
+		FlagWithRspFileInputList("-r ", rspFile, snapshotOutputs)
+
+	zipRule.Build(zipPath.String(), baseName+" snapshot "+zipPath.String())
+	return android.OptionalPathForPath(zipPath)
+}