@Test public void testImplicitDepsAreAddedCorrectly() throws NoSuchBuildTargetException { Description<GenruleDescription.Arg> genruleDescription = new GenruleDescription(); Map<String, Object> instance = ImmutableMap.<String, Object>of( "srcs", ImmutableList.of(":baz", "//biz:baz"), "out", "AndroidManifest.xml", "cmd", "$(exe //bin:executable) $(location :arg)"); ProjectFilesystem projectFilesystem = new AllExistingProjectFilesystem(); BuildRuleFactoryParams params = new BuildRuleFactoryParams(projectFilesystem, BuildTargetFactory.newInstance("//foo:bar")); ConstructorArgMarshaller marshaller = new ConstructorArgMarshaller( new DefaultTypeCoercerFactory(ObjectMappers.newDefaultInstance())); ImmutableSet.Builder<BuildTarget> declaredDeps = ImmutableSet.builder(); ImmutableSet.Builder<VisibilityPattern> visibilityPatterns = ImmutableSet.builder(); GenruleDescription.Arg constructorArg = genruleDescription.createUnpopulatedConstructorArg(); try { marshaller.populate( createCellRoots(projectFilesystem), projectFilesystem, params, constructorArg, declaredDeps, visibilityPatterns, instance); } catch (ConstructorArgMarshalException e) { fail("Expected constructorArg to be correctly populated."); } TargetNode<GenruleDescription.Arg> targetNode = new TargetNodeFactory(new DefaultTypeCoercerFactory(ObjectMappers.newDefaultInstance())) .create( Hashing.sha1().hashString(params.target.getFullyQualifiedName(), UTF_8), genruleDescription, constructorArg, params, declaredDeps.build(), visibilityPatterns.build(), createCellRoots(projectFilesystem)); assertEquals( "SourcePaths and targets from cmd string should be extracted as extra deps.", ImmutableSet.of("//foo:baz", "//biz:baz", "//bin:executable", "//foo:arg"), FluentIterable.from(targetNode.getExtraDeps()) .transform(Functions.toStringFunction()) .toSet()); }
public class HttpArtifactCacheEventTest { private static final ObjectMapper JSON_CONVERTER = ObjectMappers.newDefaultInstance(); private static final ImmutableList<RuleKey> TEST_RULE_KEYS = ImmutableList.of(new RuleKey("1234567890"), new RuleKey("123456"), new RuleKey("1234")); private static final String TEST_RULE_KEYS_JSON = "[\"1234567890\",\"123456\",\"1234\"]"; @Test public void jsonRepresentationContainsAllRuleKeysWithTransform() throws IOException { Iterable<RuleKey> ruleKeysInATransform = Iterables.transform(TEST_RULE_KEYS, Functions.<RuleKey>identity()); HttpArtifactCacheEvent.Finished finishedEvent = createBuilder().setRuleKeys(ruleKeysInATransform).build(); configureEvent(finishedEvent); String json = JSON_CONVERTER.writeValueAsString(finishedEvent); assertTrue(json.contains(TEST_RULE_KEYS_JSON)); } @Test public void jsonRepresentationContainsAllRuleKeys() throws IOException { HttpArtifactCacheEvent.Finished finishedEvent = createBuilder().setRuleKeys(TEST_RULE_KEYS).build(); configureEvent(finishedEvent); String json = JSON_CONVERTER.writeValueAsString(finishedEvent); assertTrue(json.contains(TEST_RULE_KEYS_JSON)); } private static HttpArtifactCacheEvent.Finished.Builder createBuilder() { HttpArtifactCacheEvent.Scheduled scheduledEvent = HttpArtifactCacheEvent.newStoreScheduledEvent( Optional.of("target"), ImmutableSet.<RuleKey>of()); HttpArtifactCacheEvent.Started startedEvent = HttpArtifactCacheEvent.newStoreStartedEvent(scheduledEvent); configureEvent(startedEvent); return HttpArtifactCacheEvent.newFinishedEventBuilder(startedEvent); } private static void configureEvent(AbstractBuckEvent event) { event.configure(-1, -1, -1, new BuildId()); } }
/** * {@link DexProducedFromJavaLibrary} is a {@link BuildRule} that serves a very specific purpose: it * takes a {@link JavaLibrary} and dexes the output of the {@link JavaLibrary} if its list of * classes is non-empty. Because it is expected to be used with pre-dexing, we always pass the * {@code --force-jumbo} flag to {@code dx} in this buildable. * * <p>Most {@link BuildRule}s can determine the (possibly null) path to their output file from their * definition. This is an anomaly because we do not know whether this will write a {@code .dex} file * until runtime. Unfortunately, because there is no such thing as an empty {@code .dex} file, we * cannot write a meaningful "dummy .dex" if there are no class files to pass to {@code dx}. */ public class DexProducedFromJavaLibrary extends AbstractBuildRule implements AbiRule, HasBuildTarget, InitializableFromDisk<BuildOutput> { private static final ObjectMapper MAPPER = ObjectMappers.newDefaultInstance(); private static final Function<String, HashCode> TO_HASHCODE = new Function<String, HashCode>() { @Override public HashCode apply(String input) { return HashCode.fromString(input); } }; @VisibleForTesting static final String LINEAR_ALLOC_KEY_ON_DISK_METADATA = "linearalloc"; static final String CLASSNAMES_TO_HASHES = "classnames_to_hashes"; private final JavaLibrary javaLibrary; private final BuildOutputInitializer<BuildOutput> buildOutputInitializer; @VisibleForTesting DexProducedFromJavaLibrary( BuildRuleParams params, SourcePathResolver resolver, JavaLibrary javaLibrary) { super(params, resolver); this.javaLibrary = javaLibrary; this.buildOutputInitializer = new BuildOutputInitializer<>(params.getBuildTarget(), this); } @Override public ImmutableList<Step> getBuildSteps( BuildContext context, final BuildableContext buildableContext) { ImmutableList.Builder<Step> steps = ImmutableList.builder(); steps.add(new RmStep(getProjectFilesystem(), getPathToDex(), /* shouldForceDeletion */ true)); // Make sure that the buck-out/gen/ directory exists for this.buildTarget. steps.add(new MkdirStep(getProjectFilesystem(), getPathToDex().getParent())); // If there are classes, run dx. final ImmutableSortedMap<String, HashCode> classNamesToHashes = javaLibrary.getClassNamesToHashes(); final boolean hasClassesToDx = !classNamesToHashes.isEmpty(); final Supplier<Integer> linearAllocEstimate; if (hasClassesToDx) { Path pathToOutputFile = javaLibrary.getPathToOutput(); EstimateLinearAllocStep estimate = new EstimateLinearAllocStep(getProjectFilesystem(), pathToOutputFile); steps.add(estimate); linearAllocEstimate = estimate; // To be conservative, use --force-jumbo for these intermediate .dex files so that they can be // merged into a final classes.dex that uses jumbo instructions. DxStep dx = new DxStep( getProjectFilesystem(), getPathToDex(), Collections.singleton(pathToOutputFile), EnumSet.of( DxStep.Option.USE_CUSTOM_DX_IF_AVAILABLE, DxStep.Option.RUN_IN_PROCESS, DxStep.Option.NO_OPTIMIZE, DxStep.Option.FORCE_JUMBO)); steps.add(dx); // The `DxStep` delegates to android tools to build a ZIP with timestamps in it, making // the output non-deterministic. So use an additional scrubbing step to zero these out. steps.add(new ZipScrubberStep(getProjectFilesystem(), getPathToDex())); } else { linearAllocEstimate = Suppliers.ofInstance(0); } // Run a step to record artifacts and metadata. The values recorded depend upon whether dx was // run. String stepName = hasClassesToDx ? "record_dx_success" : "record_empty_dx"; AbstractExecutionStep recordArtifactAndMetadataStep = new AbstractExecutionStep(stepName) { @Override public StepExecutionResult execute(ExecutionContext context) throws IOException { if (hasClassesToDx) { buildableContext.recordArtifact(getPathToDex()); } buildableContext.addMetadata( LINEAR_ALLOC_KEY_ON_DISK_METADATA, String.valueOf(linearAllocEstimate.get())); // Record the classnames to hashes map. buildableContext.addMetadata( CLASSNAMES_TO_HASHES, context .getObjectMapper() .writeValueAsString( Maps.transformValues(classNamesToHashes, Functions.toStringFunction()))); return StepExecutionResult.SUCCESS; } }; steps.add(recordArtifactAndMetadataStep); return steps.build(); } @Override public BuildOutput initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) throws IOException { int linearAllocEstimate = Integer.parseInt(onDiskBuildInfo.getValue(LINEAR_ALLOC_KEY_ON_DISK_METADATA).get()); Map<String, String> map = MAPPER.readValue( onDiskBuildInfo.getValue(CLASSNAMES_TO_HASHES).get(), new TypeReference<Map<String, String>>() {}); Map<String, HashCode> classnamesToHashes = Maps.transformValues(map, TO_HASHCODE); return new BuildOutput(linearAllocEstimate, ImmutableSortedMap.copyOf(classnamesToHashes)); } @Override public BuildOutputInitializer<BuildOutput> getBuildOutputInitializer() { return buildOutputInitializer; } static class BuildOutput { private final int linearAllocEstimate; private final ImmutableSortedMap<String, HashCode> classnamesToHashes; BuildOutput(int linearAllocEstimate, ImmutableSortedMap<String, HashCode> classnamesToHashes) { this.linearAllocEstimate = linearAllocEstimate; this.classnamesToHashes = classnamesToHashes; } } @Override @Nullable public Path getPathToOutput() { // A .dex file is not guaranteed to be generated, so we return null to be conservative. return null; } public Path getPathToDex() { return BuildTargets.getGenPath(getProjectFilesystem(), getBuildTarget(), "%s.dex.jar"); } public boolean hasOutput() { return !getClassNames().isEmpty(); } ImmutableSortedMap<String, HashCode> getClassNames() { // TODO(bolinfest): Assert that this Buildable has been built. Currently, there is no way to do // that from a Buildable (but there is from an AbstractCachingBuildRule). return buildOutputInitializer.getBuildOutput().classnamesToHashes; } int getLinearAllocEstimate() { return buildOutputInitializer.getBuildOutput().linearAllocEstimate; } /** * The only dep for this rule should be {@link #javaLibrary}. Therefore, the ABI key for the deps * of this buildable is the hash of the {@code .class} files for {@link #javaLibrary}. */ @Override public Sha1HashCode getAbiKeyForDeps(DefaultRuleKeyBuilderFactory defaultRuleKeyBuilderFactory) { return computeAbiKey(javaLibrary.getClassNamesToHashes()); } @VisibleForTesting static Sha1HashCode computeAbiKey(ImmutableSortedMap<String, HashCode> classNames) { Hasher hasher = Hashing.sha1().newHasher(); for (Map.Entry<String, HashCode> entry : classNames.entrySet()) { hasher.putUnencodedChars(entry.getKey()); hasher.putByte((byte) 0); hasher.putUnencodedChars(entry.getValue().toString()); hasher.putByte((byte) 0); } return Sha1HashCode.fromHashCode(hasher.hash()); } }