Skip to content

Commit

Permalink
java_binary now supports long classpath that exceeds command line arg…
Browse files Browse the repository at this point in the history
…ment length limit

When classpath length exceeds the command line length limit, we create
an empty classpath jar whose manifest contaning the whole classpath
value.

See: bazelbuild#2242
Fixed: bazelbuild#1780

--
Change-Id: Id6dd503c17f7f17e4a4c37fa01da24e2a1ea2155
Reviewed-on: https://cr.bazel.build/8353
PiperOrigin-RevId: 145521892
MOS_MIGRATED_REVID=145521892
  • Loading branch information
meteorcloudy authored and laszlocsomor committed Jan 25, 2017
1 parent 29e2294 commit d9a7d3a
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -186,16 +186,26 @@ public Artifact createStubAction(
arguments.add(Substitution.of("%javabin%", javaExecutable));
arguments.add(Substitution.of("%needs_runfiles%",
ruleContext.getFragment(Jvm.class).getJavaExecutable().isAbsolute() ? "0" : "1"));

final String classpathJarPath =
javaCommon.needClasspathJar()
? ruleContext.getRelatedArtifact(executable.getRootRelativePath(), "-classpath.jar")
.getPath().toString()
: null;

arguments.add(
new ComputedSubstitution("%classpath%") {
@Override
public String getValue() {
StringBuilder buffer = new StringBuilder();
Iterable<Artifact> jars = javaCommon.getRuntimeClasspath();
char delimiter = File.pathSeparatorChar;
appendRunfilesRelativeEntries(
buffer, jars, workspacePrefix, delimiter, isRunfilesEnabled);
return buffer.toString();
if (javaCommon.needClasspathJar()) {
return classpathJarPath;
} else {
StringBuilder classpathValue = new StringBuilder();
char delimiter = File.pathSeparatorChar;
appendRunfilesRelativeEntries(
classpathValue, javaCommon.getRuntimeClasspath(), delimiter, isRunfilesEnabled);
return classpathValue.toString();
}
}
});

Expand Down Expand Up @@ -250,17 +260,18 @@ public String getValue() {

/**
* Builds a class path by concatenating the root relative paths of the artifacts separated by the
* delimiter. Each relative path entry is prepended with "${RUNPATH}" which will be expanded by
* the stub script at runtime, to either "${JAVA_RUNFILES}/" or if we are lucky, the empty string.
* delimiter. If runfiles is disabled(eg. Windows) we return the absolute path, otherwise each
* relative path entry is prepended with "${RUNPATH}" which will be expanded by the stub script at
* runtime, to either "${JAVA_RUNFILES}/" or if we are lucky, the empty string.
*
* @param buffer the buffer to use for concatenating the entries
* @param artifacts the entries to concatenate in the buffer
* @param delimiter the delimiter character to separate the entries
* @param isRunfilesEnabled true if runfiles is enabled
*/
private static void appendRunfilesRelativeEntries(
StringBuilder buffer,
Iterable<Artifact> artifacts,
String workspacePrefix,
char delimiter,
boolean isRunfilesEnabled) {
buffer.append("\"");
Expand All @@ -269,11 +280,8 @@ private static void appendRunfilesRelativeEntries(
buffer.append(delimiter);
}
if (!isRunfilesEnabled) {
buffer.append("$(rlocation ");
PathFragment runfilePath =
new PathFragment(new PathFragment(workspacePrefix), artifact.getRunfilesPath());
buffer.append(runfilePath.normalize().getPathString());
buffer.append(")");
// If runfiles directory doesn't exist (eg. Windows), return the absolute path.
buffer.append(artifact.getPath());
} else {
buffer.append("${RUNPATH}");
buffer.append(artifact.getRunfilesPath().getPathString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,16 @@ public ConfiguredTarget create(RuleContext ruleContext)
filesBuilder.add(executableToRun);
runfilesBuilder.addArtifact(executableToRun);
}

if (common.needClasspathJar()) {
Artifact classpathJar = helper.createClasspathJar(executableForRunfiles);

StringBuilder manifestFileContent = common.getClasspathEntryForManifestFile();
helper.createClasspathJarAction(classpathJar, manifestFileContent.toString());

filesBuilder.add(classpathJar);
runfilesBuilder.addArtifact(classpathJar);
}
}

JavaSourceJarsProvider sourceJarsProvider = javaSourceJarsProviderBuilder.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@
import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.util.FileTypeSet;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
Expand Down Expand Up @@ -110,6 +112,12 @@ public void collectMetadataArtifacts(Iterable<Artifact> objectFiles,
private final JavaSemantics semantics;
private JavaCompilationHelper javaCompilationHelper;

// Windows per-arg limit MAX_ARG_STRLEN == 8k
// Linux per-arg limit MAX_ARG_STRLEN == 128k
private static final int MAX_ARG_STRLEN = (OS.getCurrent() == OS.WINDOWS) ? 7000 : 120000;
private Boolean needClasspathJarValue;


public JavaCommon(RuleContext ruleContext, JavaSemantics semantics) {
this(ruleContext, semantics,
ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET).list(),
Expand Down Expand Up @@ -889,6 +897,48 @@ public NestedSet<Artifact> getRuntimeClasspath() {
return classpathFragment.getRuntimeClasspath();
}

/**
* Compute the total length of all class paths (considering the worst case where every path is
* absolute), return true if it exceeds the max command line argument length limit.
*/
public boolean needClasspathJar() {
if (needClasspathJarValue == null) {
Iterable<Artifact> artifacts = classpathFragment.getRuntimeClasspath();
int length = 0;
for (Artifact artifact : artifacts) {
length += artifact.getPath().toString().length() + 1;
}
needClasspathJarValue = length > MAX_ARG_STRLEN;
}
return needClasspathJarValue;
}

/** Adds line breaks to enforce a maximum 72 bytes per line. */
public static StringBuilder make72Safe(StringBuilder line) {
StringBuilder result = new StringBuilder();
int length = line.length();
for (int i = 0; i < length; i += 69) {
result.append(line, i, Math.min(i + 69, length));
result.append("\r\n ");
}
return result;
}

/**
* Build the entry for classpath in jar MANIFEST.MF file, spaces in path should be escaped since
* it's the delimiter.
*/
public StringBuilder getClasspathEntryForManifestFile() {
StringBuilder buffer = new StringBuilder();
buffer.append("Class-Path:");
for (Artifact artifact : classpathFragment.getRuntimeClasspath()) {
buffer.append(" ");
buffer.append(Paths.get(artifact.getPath().toString()).toUri());
}
buffer.append("\r\n");
return make72Safe(buffer);
}

public NestedSet<Artifact> getCompileTimeClasspath() {
return classpathFragment.getCompileTimeClasspath();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration.StrictDepsMode;
Expand Down Expand Up @@ -389,6 +390,11 @@ public Artifact createGensrcJar(Artifact outputJar) {
outputJar.getRoot());
}

/** Returns the artifact for an empty jar file containing classpath value in its manifest file. */
public Artifact createClasspathJar(Artifact executable) {
return getRuleContext().getRelatedArtifact(executable.getRootRelativePath(), "-classpath.jar");
}

/**
* Returns whether this target uses annotation processing.
*/
Expand Down Expand Up @@ -447,6 +453,44 @@ public void createGenJarAction(Artifact classJar, Artifact manifestProto,
.build(getRuleContext()));
}

/**
* Creating the action for creating the classpath jar whose manifest file containing a long
* classpath value.
*
* @param classpathJar The artifact for the classpath jar emitted from JavaBuilder
* @param manifestFileContent The content of the manifest file in the classpath jar
*/
public void createClasspathJarAction(Artifact classpathJar, CharSequence manifestFileContent) {
Artifact manifestFile =
ruleContext.getRelatedArtifact(classpathJar.getRootRelativePath(), ".jar_manifest_file");

ruleContext.registerAction(
FileWriteAction.create(ruleContext, manifestFile, manifestFileContent, false));

getRuleContext()
.registerAction(
new SpawnAction.Builder()
.addInput(manifestFile)
.addOutput(classpathJar)
.addTransitiveInputs(getHostJavabaseInputs(getRuleContext()))
.setJarExecutable(
getRuleContext()
.getHostConfiguration()
.getFragment(Jvm.class)
.getJavaExecutable(),
javaToolchain.getJavaBuilder(),
javaToolchain.getJvmOptions())
.setCommandLine(
CustomCommandLine.builder()
.addExecPath("--manifest_file", manifestFile)
.addExecPath("--output", classpathJar)
.build())
.useParameterFile(ParameterFileType.SHELL_QUOTED)
.setProgressMessage("Building classpath jar " + classpathJar.prettyPrint())
.setMnemonic("JavaClasspathJar")
.build(getRuleContext()));
}

/** Returns the GenClass deploy jar Artifact. */
private Artifact getGenClassJar(RuleContext ruleContext) {
Artifact genClass = javaToolchain.getGenClass();
Expand Down
49 changes: 49 additions & 0 deletions src/test/shell/bazel/bazel_java_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,55 @@ public class HelloLibrary {
EOF
}

function write_hello_library_files_for_long_classpath() {
mkdir -p java/hello_library
cat >java/hello_library/BUILD <<EOF
package(default_visibility=['//visibility:public'])
EOF
deps=""
for i in `seq 1 1000`; do
deps="'//java/hello_library:hello_library$i', $deps"
cat >>java/hello_library/BUILD <<EOF
java_library(name = 'hello_library$i',
srcs = ['HelloLibrary$i.java']);
EOF
cat >java/hello_library/HelloLibrary$i.java <<EOF
package hello_library;
public class HelloLibrary$i {
public static void funcHelloLibrary() {
System.out.print("Hello, Library!;");
}
}
EOF
done

mkdir -p java/main
cat >java/main/BUILD <<EOF
java_binary(name = 'main',
deps = [$deps],
srcs = ['Main.java'],
main_class = 'main.Main')
EOF

cat >java/main/Main.java <<EOF
package main;
import hello_library.HelloLibrary1;
public class Main {
public static void main(String[] args) {
HelloLibrary1.funcHelloLibrary();
System.out.println("Hello, World!");
}
}
EOF
}

function test_build_hello_world_with_long_classpath() {
write_hello_library_files_for_long_classpath

bazel build //java/main:main &> $TEST_log || fail "build failed"
assert_bazel_run "//java/main:main" "Hello, Library!;Hello, World!"
}

function test_build_hello_world() {
write_hello_library_files

Expand Down

0 comments on commit d9a7d3a

Please sign in to comment.