Export provenance metadata for prebuilt APKs and APEXes.

Bug: 217434690
Test: atest --host gen_provenance_metadata_test
Test: m provenance_metadata

Change-Id: I91c184b6e6fe5ccfc3fc65b55b09e7a3da9502a0
This commit is contained in:
Wei Li
2022-03-18 17:33:24 -07:00
parent 85b935eff2
commit 340ee8e699
15 changed files with 672 additions and 41 deletions

36
provenance/Android.bp Normal file
View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2022 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 {
default_applicable_licenses: ["Android-Apache-2.0"],
}
bootstrap_go_package {
name: "soong-provenance",
pkgPath: "android/soong/provenance",
srcs: [
"provenance_singleton.go",
],
deps: [
"soong-android",
],
testSrcs: [
"provenance_singleton_test.go",
],
pluginFor: [
"soong_build",
],
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2022 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 {
default_applicable_licenses: ["Android-Apache-2.0"],
}
python_library_host {
name: "provenance_metadata_proto",
version: {
py3: {
enabled: true,
},
},
srcs: [
"provenance_metadata.proto",
],
proto: {
canonical_path_from_root: false,
},
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2022 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.
*/
syntax = "proto3";
package provenance_metadata_proto;
option go_package = "android/soong/provenance/provenance_metadata_proto";
// Provenance metadata of artifacts.
message ProvenanceMetadata {
// Name of the module/target that creates the artifact.
// It is either a Soong module name or Bazel target label.
string module_name = 1;
// The path to the prebuilt artifacts, which is relative to the source tree
// directory. For example, “prebuilts/runtime/mainline/i18n/apex/com.android.i18n-arm.apex”.
string artifact_path = 2;
// The SHA256 hash of the artifact.
string artifact_sha256 = 3;
// The install path of the artifact in filesystem images.
// This is the absolute path of the artifact on the device.
string artifact_install_path = 4;
// Path of the attestation file of a prebuilt artifact, which is relative to
// the source tree directory. This is for prebuilt artifacts which have
// corresponding attestation files checked in the source tree.
string attestation_path = 5;
}
message ProvenanceMetaDataList {
repeated ProvenanceMetadata metadata = 1;
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright (C) 2022 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 provenance
import (
"android/soong/android"
"github.com/google/blueprint"
)
var (
pctx = android.NewPackageContext("android/soong/provenance")
rule = pctx.HostBinToolVariable("gen_provenance_metadata", "gen_provenance_metadata")
genProvenanceMetaData = pctx.AndroidStaticRule("genProvenanceMetaData",
blueprint.RuleParams{
Command: `rm -rf "$out" && ` +
`${gen_provenance_metadata} --module_name=${module_name} ` +
`--artifact_path=$in --install_path=${install_path} --metadata_path=$out`,
CommandDeps: []string{"${gen_provenance_metadata}"},
}, "module_name", "install_path")
mergeProvenanceMetaData = pctx.AndroidStaticRule("mergeProvenanceMetaData",
blueprint.RuleParams{
Command: `rm -rf $out $out.temp && ` +
`echo -e "# proto-file: build/soong/provenance/proto/provenance_metadata.proto\n# proto-message: ProvenanceMetaDataList" > $out && ` +
`touch $out.temp && cat $out.temp $in | grep -v "^#.*" >> $out && rm -rf $out.temp`,
})
)
type ProvenanceMetadata interface {
ProvenanceMetaDataFile() android.OutputPath
}
func init() {
RegisterProvenanceSingleton(android.InitRegistrationContext)
}
func RegisterProvenanceSingleton(ctx android.RegistrationContext) {
ctx.RegisterSingletonType("provenance_metadata_singleton", provenanceInfoSingletonFactory)
}
var PrepareForTestWithProvenanceSingleton = android.FixtureRegisterWithContext(RegisterProvenanceSingleton)
func provenanceInfoSingletonFactory() android.Singleton {
return &provenanceInfoSingleton{}
}
type provenanceInfoSingleton struct {
}
func (b *provenanceInfoSingleton) GenerateBuildActions(context android.SingletonContext) {
allMetaDataFiles := make([]android.Path, 0)
context.VisitAllModulesIf(moduleFilter, func(module android.Module) {
if p, ok := module.(ProvenanceMetadata); ok {
allMetaDataFiles = append(allMetaDataFiles, p.ProvenanceMetaDataFile())
}
})
mergedMetaDataFile := android.PathForOutput(context, "provenance_metadata.textproto")
context.Build(pctx, android.BuildParams{
Rule: mergeProvenanceMetaData,
Description: "merge provenance metadata",
Inputs: allMetaDataFiles,
Output: mergedMetaDataFile,
})
context.Build(pctx, android.BuildParams{
Rule: blueprint.Phony,
Description: "phony rule of merge provenance metadata",
Inputs: []android.Path{mergedMetaDataFile},
Output: android.PathForPhony(context, "provenance_metadata"),
})
}
func moduleFilter(module android.Module) bool {
if !module.Enabled() || module.IsSkipInstall() {
return false
}
if p, ok := module.(ProvenanceMetadata); ok {
return p.ProvenanceMetaDataFile().String() != ""
}
return false
}
func GenerateArtifactProvenanceMetaData(ctx android.ModuleContext, artifactPath android.Path, installedFile android.InstallPath) android.OutputPath {
onDevicePathOfInstalledFile := android.InstallPathToOnDevicePath(ctx, installedFile)
artifactMetaDataFile := android.PathForIntermediates(ctx, "provenance_metadata", ctx.ModuleDir(), ctx.ModuleName(), "provenance_metadata.textproto")
ctx.Build(pctx, android.BuildParams{
Rule: genProvenanceMetaData,
Description: "generate artifact provenance metadata",
Inputs: []android.Path{artifactPath},
Output: artifactMetaDataFile,
Args: map[string]string{
"module_name": ctx.ModuleName(),
"install_path": onDevicePathOfInstalledFile,
}})
return artifactMetaDataFile
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2022 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 provenance
import (
"strings"
"testing"
"android/soong/android"
)
func TestProvenanceSingleton(t *testing.T) {
result := android.GroupFixturePreparers(
PrepareForTestWithProvenanceSingleton,
android.PrepareForTestWithAndroidMk).RunTestWithBp(t, "")
outputs := result.SingletonForTests("provenance_metadata_singleton").AllOutputs()
for _, output := range outputs {
testingBuildParam := result.SingletonForTests("provenance_metadata_singleton").Output(output)
switch {
case strings.Contains(output, "soong/provenance_metadata.textproto"):
android.AssertStringEquals(t, "Invalid build rule", "android/soong/provenance.mergeProvenanceMetaData", testingBuildParam.Rule.String())
android.AssertIntEquals(t, "Invalid input", len(testingBuildParam.Inputs), 0)
android.AssertStringDoesContain(t, "Invalid output path", output, "soong/provenance_metadata.textproto")
android.AssertIntEquals(t, "Invalid args", len(testingBuildParam.Args), 0)
case strings.HasSuffix(output, "provenance_metadata"):
android.AssertStringEquals(t, "Invalid build rule", "<builtin>:phony", testingBuildParam.Rule.String())
android.AssertStringEquals(t, "Invalid input", testingBuildParam.Inputs[0].String(), "out/soong/provenance_metadata.textproto")
android.AssertStringEquals(t, "Invalid output path", output, "provenance_metadata")
android.AssertIntEquals(t, "Invalid args", len(testingBuildParam.Args), 0)
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright (C) 2022 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.
*/
python_binary_host {
name: "gen_provenance_metadata",
srcs: [
"gen_provenance_metadata.py",
],
version: {
py3: {
embedded_launcher: true,
},
},
libs: [
"provenance_metadata_proto",
"libprotobuf-python",
],
}
python_test_host {
name: "gen_provenance_metadata_test",
main: "gen_provenance_metadata_test.py",
srcs: [
"gen_provenance_metadata_test.py",
],
data: [
":gen_provenance_metadata",
],
libs: [
"provenance_metadata_proto",
"libprotobuf-python",
],
test_suites: ["general-tests"],
}

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env python3
#
# Copyright (C) 2022 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.
import argparse
import hashlib
import sys
import google.protobuf.text_format as text_format
import provenance_metadata_pb2
def Log(*info):
if args.verbose:
for i in info:
print(i)
def ParseArgs(argv):
parser = argparse.ArgumentParser(description='Create provenance metadata for a prebuilt artifact')
parser.add_argument('-v', '--verbose', action='store_true', help='Print more information in execution')
parser.add_argument('--module_name', help='Module name', required=True)
parser.add_argument('--artifact_path', help='Relative path of the prebuilt artifact in source tree', required=True)
parser.add_argument('--install_path', help='Absolute path of the artifact in the filesystem images', required=True)
parser.add_argument('--metadata_path', help='Path of the provenance metadata file created for the artifact', required=True)
return parser.parse_args(argv)
def main(argv):
global args
args = ParseArgs(argv)
Log("Args:", vars(args))
provenance_metadata = provenance_metadata_pb2.ProvenanceMetadata()
provenance_metadata.module_name = args.module_name
provenance_metadata.artifact_path = args.artifact_path
provenance_metadata.artifact_install_path = args.install_path
Log("Generating SHA256 hash")
h = hashlib.sha256()
with open(args.artifact_path, "rb") as artifact_file:
h.update(artifact_file.read())
provenance_metadata.artifact_sha256 = h.hexdigest()
text_proto = [
"# proto-file: build/soong/provenance/proto/provenance_metadata.proto",
"# proto-message: ProvenanceMetaData",
"",
text_format.MessageToString(provenance_metadata)
]
with open(args.metadata_path, "wt") as metadata_file:
file_content = "\n".join(text_proto)
Log("Writing provenance metadata in textproto:", file_content)
metadata_file.write(file_content)
if __name__ == '__main__':
main(sys.argv[1:])

View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python3
#
# Copyright (C) 2022 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.
import hashlib
import logging
import os
import subprocess
import tempfile
import unittest
import google.protobuf.text_format as text_format
import provenance_metadata_pb2
logger = logging.getLogger(__name__)
def run(args, verbose=None, **kwargs):
"""Creates and returns a subprocess.Popen object.
Args:
args: The command represented as a list of strings.
verbose: Whether the commands should be shown. Default to the global
verbosity if unspecified.
kwargs: Any additional args to be passed to subprocess.Popen(), such as env,
stdin, etc. stdout and stderr will default to subprocess.PIPE and
subprocess.STDOUT respectively unless caller specifies any of them.
universal_newlines will default to True, as most of the users in
releasetools expect string output.
Returns:
A subprocess.Popen object.
"""
if 'stdout' not in kwargs and 'stderr' not in kwargs:
kwargs['stdout'] = subprocess.PIPE
kwargs['stderr'] = subprocess.STDOUT
if 'universal_newlines' not in kwargs:
kwargs['universal_newlines'] = True
if verbose:
logger.info(" Running: \"%s\"", " ".join(args))
return subprocess.Popen(args, **kwargs)
def run_and_check_output(args, verbose=None, **kwargs):
"""Runs the given command and returns the output.
Args:
args: The command represented as a list of strings.
verbose: Whether the commands should be shown. Default to the global
verbosity if unspecified.
kwargs: Any additional args to be passed to subprocess.Popen(), such as env,
stdin, etc. stdout and stderr will default to subprocess.PIPE and
subprocess.STDOUT respectively unless caller specifies any of them.
Returns:
The output string.
Raises:
ExternalError: On non-zero exit from the command.
"""
proc = run(args, verbose=verbose, **kwargs)
output, _ = proc.communicate()
if output is None:
output = ""
if verbose:
logger.info("%s", output.rstrip())
if proc.returncode != 0:
raise RuntimeError(
"Failed to run command '{}' (exit code {}):\n{}".format(
args, proc.returncode, output))
return output
def run_host_command(args, verbose=None, **kwargs):
host_build_top = os.environ.get("ANDROID_BUILD_TOP")
if host_build_top:
host_command_dir = os.path.join(host_build_top, "out/host/linux-x86/bin")
args[0] = os.path.join(host_command_dir, args[0])
return run_and_check_output(args, verbose, **kwargs)
def sha256(s):
h = hashlib.sha256()
h.update(bytearray(s, 'utf-8'))
return h.hexdigest()
class ProvenanceMetaDataToolTest(unittest.TestCase):
def test_gen_provenance_metadata(self):
artifact_content = "test artifact"
artifact_file = tempfile.mktemp()
with open(artifact_file,"wt") as f:
f.write(artifact_content)
metadata_file = tempfile.mktemp()
cmd = ["gen_provenance_metadata"]
cmd.extend(["--module_name", "a"])
cmd.extend(["--artifact_path", artifact_file])
cmd.extend(["--install_path", "b"])
cmd.extend(["--metadata_path", metadata_file])
output = run_host_command(cmd)
self.assertEqual(output, "")
with open(metadata_file,"rt") as f:
data = f.read()
provenance_metadata = provenance_metadata_pb2.ProvenanceMetadata()
text_format.Parse(data, provenance_metadata)
self.assertEqual(provenance_metadata.module_name, "a")
self.assertEqual(provenance_metadata.artifact_path, artifact_file)
self.assertEqual(provenance_metadata.artifact_install_path, "b")
self.assertEqual(provenance_metadata.artifact_sha256, sha256(artifact_content))
os.remove(artifact_file)
os.remove(metadata_file)
if __name__ == '__main__':
unittest.main(verbosity=2)