Skip to content

Commit

Permalink
Create new android_instrumentation rule.
Browse files Browse the repository at this point in the history
This rule is responsible for building the target and instrumentation APKs used by an Android instrumentation test. If they are provided as APKs (e.g. from an android_binary or a genrule) they will be used as is. If they are provided as libraries, APKs will be created. This CL does not actually implement building target and instrumentation APKs from libraries, that will come in a follow-up CL as it will require some heavy refactoring of AndroidBinary.java.

Follow-up CLs will add features such as repackaging the APKs to remove duplicate classes, reproguarding the target APK with the test code, validating that the target and instrumentation APKs were signed with the same debug key and verifying that instrumentation stanza appears in the instrumentation APKs manifest.

Note that this CL does _not_ install the rule in the BazelRuleClassProvider, so
this CL does not make it usable by anyone. Once the other android testing rules are ready, I will install them all.

One small step towards bazelbuild#903.

RELNOTES: None
PiperOrigin-RevId: 155220900
  • Loading branch information
aj-michael authored and damienmg committed May 5, 2017
1 parent b89f9fa commit 4b3f9db
Show file tree
Hide file tree
Showing 6 changed files with 450 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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 com.google.devtools.build.lib.rules.android;

import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.FileProvider;
import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.RunfilesProvider;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.actions.SymlinkAction;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SafeImplicitOutputsFunction;
import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
import com.google.devtools.build.lib.util.FileType;

/**
* An implementation of the {@code android_instrumentation} rule.
*/
public class AndroidInstrumentation implements RuleConfiguredTargetFactory {

private static final SafeImplicitOutputsFunction TARGET_APK = ImplicitOutputsFunction
.fromTemplates("%{name}-target.apk");
private static final SafeImplicitOutputsFunction INSTRUMENTATION_APK =
ImplicitOutputsFunction.fromTemplates("%{name}-instrumentation.apk");
static final SafeImplicitOutputsFunction IMPLICIT_OUTPUTS_FUNCTION =
ImplicitOutputsFunction.fromFunctions(TARGET_APK, INSTRUMENTATION_APK);

@Override
public ConfiguredTarget create(RuleContext ruleContext)
throws InterruptedException, RuleErrorException {

Artifact targetApk = getTargetApk(ruleContext);
Artifact instrumentationApk = createInstrumentationApk(ruleContext);

RuleConfiguredTargetBuilder ruleBuilder = new RuleConfiguredTargetBuilder(ruleContext);
return ruleBuilder
.setFilesToBuild(
NestedSetBuilder.<Artifact>stableOrder().add(targetApk).add(instrumentationApk).build())
.addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
.addNativeDeclaredProvider(
new AndroidInstrumentationInfoProvider(targetApk, instrumentationApk))
.build();
}

private static boolean exactlyOneOf(boolean expression1, boolean expression2) {
return (expression1 && !expression2) || (!expression1 && expression2);
}

/**
* Returns the APK from the {@code target} attribute or creates one from the {@code
* target_library} attribute.
*/
private static Artifact getTargetApk(RuleContext ruleContext)
throws RuleErrorException, InterruptedException {
Artifact apk = ruleContext.getImplicitOutputArtifact(TARGET_APK);
TransitiveInfoCollection target = ruleContext.getPrerequisite("target", Mode.TARGET);
TransitiveInfoCollection targetLibrary =
ruleContext.getPrerequisite("target_library", Mode.TARGET);

if (!exactlyOneOf(target == null, targetLibrary == null)) {
ruleContext.throwWithRuleError(
"android_instrumentation requires that exactly one of the target and target_library "
+ "attributes be specified.");
}

if (target != null) {
// target attribute is specified
symlinkApkFromApkProviderOrFile(ruleContext, target, apk, "Symlinking target APK");
} else {
// target_library attribute is specified
createApkFromLibrary(ruleContext, targetLibrary, apk);
}

return apk;
}

/**
* Returns the APK from the {@code instrumentation} attribute or creates one from the {@code
* instrumentation_library} attribute.
*/
private static Artifact createInstrumentationApk(RuleContext ruleContext)
throws RuleErrorException, InterruptedException {
Artifact apk = ruleContext.getImplicitOutputArtifact(INSTRUMENTATION_APK);
TransitiveInfoCollection instrumentation =
ruleContext.getPrerequisite("instrumentation", Mode.TARGET);
TransitiveInfoCollection instrumentationLibrary =
ruleContext.getPrerequisite("instrumentation_library", Mode.TARGET);

if (!exactlyOneOf(instrumentation == null, instrumentationLibrary == null)) {
ruleContext.throwWithRuleError(
"android_instrumentation requires that exactly one of the instrumentation and "
+ "instrumentation_library attributes be specified.");
}

if (instrumentation != null) {
// instrumentation attribute is specified
symlinkApkFromApkProviderOrFile(
ruleContext, instrumentation, apk, "Symlinking instrumentation APK");
} else {
// instrumentation_library attribute is specified
createApkFromLibrary(ruleContext, instrumentationLibrary, apk);
}

return apk;
}

// We symlink instead of simply providing the artifact as is to satisfy the implicit outputs
// function. This allows user to refer to the APK outputs of the android_instrumentation rule by
// the same name, whether they were built from libraries or simply symlinked from the output of
// an android_binary rule.
private static void symlinkApkFromApkProviderOrFile(
RuleContext ruleContext,
TransitiveInfoCollection transitiveInfoCollection,
Artifact apk,
String message) {
Artifact existingApk;
ApkProvider apkProvider = transitiveInfoCollection.getProvider(ApkProvider.class);
if (apkProvider != null) {
existingApk = Iterables.getOnlyElement(apkProvider.getTransitiveApks());
} else {
existingApk =
Iterables.getOnlyElement(
FileType.filter(
transitiveInfoCollection.getProvider(FileProvider.class).getFilesToBuild(),
AndroidRuleClasses.APK));
}

ruleContext.registerAction(
new SymlinkAction(ruleContext.getActionOwner(), existingApk, apk, message));
}

@SuppressWarnings("unused") // TODO(b/37856762): Implement APK building from libraries.
private static Artifact createApkFromLibrary(
RuleContext ruleContext, TransitiveInfoCollection library, Artifact apk)
throws RuleErrorException {
// TODO(b/37856762): Cleanup AndroidBinary#createAndroidBinary and use it here.
ruleContext.throwWithRuleError(
"android_instrumentation dependencies on android_library rules are not yet supported");
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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 com.google.devtools.build.lib.rules.android;

import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.packages.NativeClassObjectConstructor;
import com.google.devtools.build.lib.packages.SkylarkClassObject;

/**
* A provider for targets that create Android instrumentations. Consumed by {@link
* AndroidInstrumentationTest}.
*/
@Immutable
public class AndroidInstrumentationInfoProvider extends SkylarkClassObject
implements TransitiveInfoProvider {

private static final String SKYLARK_NAME = "AndroidInstrumentationInfo";
static final NativeClassObjectConstructor ANDROID_INSTRUMENTATION_INFO =
new NativeClassObjectConstructor(SKYLARK_NAME) {
};

private final Artifact targetApk;
private final Artifact instrumentationApk;

public AndroidInstrumentationInfoProvider(Artifact targetApk, Artifact instrumentationApk) {
super(ANDROID_INSTRUMENTATION_INFO, ImmutableMap.<String, Object>of());
this.targetApk = targetApk;
this.instrumentationApk = instrumentationApk;
}

public Artifact getTargetApk() {
return targetApk;
}

public Artifact getInstrumentationApk() {
return instrumentationApk;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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 com.google.devtools.build.lib.rules.android;

import static com.google.devtools.build.lib.packages.Attribute.attr;
import static com.google.devtools.build.lib.packages.BuildType.LABEL;

import com.google.devtools.build.lib.analysis.BaseRuleClasses;
import com.google.devtools.build.lib.analysis.RuleDefinition;
import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
import com.google.devtools.build.lib.packages.RuleClass;
import com.google.devtools.build.lib.packages.RuleClass.Builder;

/** Rule definition for the {@code android_instrumentation} rule. */
public class AndroidInstrumentationRule implements RuleDefinition {
@Override
public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
return builder
.setUndocumented()
.add(
attr("target", LABEL)
.allowedFileTypes(AndroidRuleClasses.APK)
.allowedRuleClasses("android_binary"))
.add(
attr("target_library", LABEL)
.allowedFileTypes()
.allowedRuleClasses("android_library"))
.add(
attr("instrumentation", LABEL)
.allowedFileTypes(AndroidRuleClasses.APK)
.allowedRuleClasses("android_binary"))
.add(
attr("instrumentation_library", LABEL)
.allowedFileTypes()
.allowedRuleClasses("android_library"))
.setImplicitOutputsFunction(AndroidInstrumentation.IMPLICIT_OUTPUTS_FUNCTION)
.build();
}

@Override
public Metadata getMetadata() {
return RuleDefinition.Metadata.builder()
.name("android_instrumentation")
.ancestors(BaseRuleClasses.RuleBase.class)
.factoryClass(AndroidInstrumentation.class)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ protected ConfiguredRuleClassProvider getRuleClassProvider() {
return builder
// TODO(b/35097211): Remove this once the new testing rules are released.
.addRuleDefinition(new AndroidDeviceScriptFixtureRule())
.addRuleDefinition(new AndroidInstrumentationRule())
.build();
}

Expand Down
Loading

0 comments on commit 4b3f9db

Please sign in to comment.