Skip to content
This repository was archived by the owner on Nov 10, 2023. It is now read-only.

Commit 5759315

Browse files
jimpurbrickbolinfest
authored andcommitted
Parse build files with Jython to avoid shelling out to Python
Summary: Modify BuildFileToJsonParser to use Jython via JSR-223 rather than creating a new process to parse each python build file. Cuts build file parsing time roughly in half and avoids buck depending on the system python. Test Plan: ant clean compile test && buck clean && buck build buck && buck test --all
1 parent 4d8bcda commit 5759315

File tree

13 files changed

+164
-54
lines changed

13 files changed

+164
-54
lines changed

.classpath

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@
1919
<classpathentry kind="lib" path="lib/hamcrest-core-1.3.jar"/>
2020
<classpathentry kind="lib" path="lib/hamcrest-library-1.3.jar"/>
2121
<classpathentry kind="lib" path="lib/sdklib.jar"/>
22+
<classpathentry kind="lib" path="lib/jyson-1.0.2.jar"/>
23+
<classpathentry kind="lib" path="lib/jython-standalone-2.5.3.jar"/>
2224
<classpathentry kind="output" path="build/classes"/>
2325
</classpath>

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ local.properties
77

88
# Compiled Python
99
*.pyc
10+
*py.class
1011

1112
# IntelliJ, based on http://devnet.jetbrains.net/docs/DOC-1186
1213
.idea/dictionaries

.idea/libraries/buck_lib.xml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bin/buck

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,5 +132,7 @@ ${BUCK_DIRECTORY}/lib/jackson-core-2.0.5.jar:\
132132
${BUCK_DIRECTORY}/lib/jackson-databind-2.0.5.jar:\
133133
${BUCK_DIRECTORY}/lib/jsr305.jar:\
134134
${BUCK_DIRECTORY}/lib/sdklib.jar:\
135-
${BUCK_DIRECTORY}/lib/ddmlib-r21.jar \
135+
${BUCK_DIRECTORY}/lib/ddmlib-r21.jar:\
136+
${BUCK_DIRECTORY}/lib/jython-standalone-2.5.3.jar:\
137+
${BUCK_DIRECTORY}/lib/jyson-1.0.2.jar \
136138
com.facebook.buck.cli.Main "$@"

build.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@
101101
<include name="hamcrest-core-1.3.jar" />
102102
<include name="hamcrest-library-1.3.jar" />
103103
<include name="objenesis-1.2.jar" />
104+
<include name="jython-standalone-2.5.3.jar" />
105+
<include name="jyson-1.0.2.jar" />
104106
</fileset>
105107
<pathelement location="${testclasses.dir}" />
106108
<pathelement location="${test.dir}" />

lib/BUCK

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,21 @@ prebuilt_jar(
145145
'//src/com/facebook/buck/shell:shell',
146146
]
147147
)
148+
149+
jython_visibility = [
150+
'//src/com/facebook/buck/shell:shell',
151+
'//test/com/facebook/buck/cli:testutil',
152+
'//test/com/facebook/buck/parser:parser',
153+
]
154+
155+
prebuilt_jar(
156+
name = 'jython',
157+
binary_jar = 'jython-standalone-2.5.3.jar',
158+
visibility = jython_visibility,
159+
)
160+
161+
prebuilt_jar(
162+
name = 'jyson',
163+
binary_jar = 'jyson-1.0.2.jar',
164+
visibility = jython_visibility,
165+
)

lib/jyson-1.0.2.jar

7.62 KB
Binary file not shown.

lib/jython-standalone-2.5.3.jar

13.7 MB
Binary file not shown.

src/com/facebook/buck/json/BuildFileToJsonParser.java

Lines changed: 74 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,33 @@
1717
package com.facebook.buck.json;
1818

1919
import com.facebook.buck.util.Ansi;
20-
import com.facebook.buck.util.InputStreamConsumer;
2120
import com.fasterxml.jackson.core.JsonFactory;
2221
import com.fasterxml.jackson.core.JsonParseException;
2322
import com.fasterxml.jackson.core.JsonParser;
2423
import com.fasterxml.jackson.core.JsonToken;
24+
import com.google.common.base.Joiner;
2525
import com.google.common.base.Optional;
2626
import com.google.common.base.Preconditions;
2727
import com.google.common.base.Throwables;
28+
import com.google.common.collect.ImmutableList;
2829
import com.google.common.collect.Lists;
2930
import com.google.common.collect.Maps;
31+
import com.google.common.io.Closer;
3032

3133
import java.io.File;
3234
import java.io.IOException;
3335
import java.io.InputStream;
36+
import java.io.PipedReader;
37+
import java.io.PipedWriter;
3438
import java.io.Reader;
3539
import java.util.List;
3640
import java.util.Map;
41+
import java.util.concurrent.ExecutorService;
42+
import java.util.concurrent.Executors;
43+
44+
import javax.script.ScriptEngine;
45+
import javax.script.ScriptEngineManager;
46+
import javax.script.ScriptException;
3747

3848
/**
3949
* This is a special JSON parser that is customized to consume the JSON output of buck.py. In
@@ -50,6 +60,16 @@ public class BuildFileToJsonParser {
5060
private static final String PATH_TO_BUCK_PY = System.getProperty("buck.path_to_buck_py",
5161
"src/com/facebook/buck/parser/buck.py");
5262

63+
private static final ExecutorService executor = Executors.newSingleThreadExecutor();
64+
private static final ScriptEngine engine = new ScriptEngineManager().getEngineByName("python");
65+
private static final String python = Joiner.on(System.getProperty("line.separator")).join(
66+
"import sys",
67+
"import os.path",
68+
"sys.path.append(os.path.dirname(\"%s\"))",
69+
"import buck",
70+
"sys.argv=[\"%s\"]",
71+
"buck.main()");
72+
5373
private final JsonParser parser;
5474

5575
public BuildFileToJsonParser(String json) throws JsonParseException, IOException {
@@ -143,6 +163,40 @@ public static List<Map<String, Object>> getAllRulesInProject(
143163
return getAllRules(rootPath.getAbsolutePath(), Optional.<String>absent(), includes, ansi);
144164
}
145165

166+
private static class BuildFileRunner implements Runnable {
167+
168+
private final ImmutableList<String> args;
169+
private PipedWriter outputWriter;
170+
171+
public BuildFileRunner(ImmutableList<String> args, PipedReader outputReader) throws IOException {
172+
this.args = args;
173+
this.outputWriter = new PipedWriter();
174+
outputWriter.connect(outputReader);
175+
}
176+
177+
@Override
178+
public void run() {
179+
Closer closer = Closer.create();
180+
try {
181+
closer.register(outputWriter);
182+
183+
// TODO(user): call buck.py directly rather than emulating the old command line interface?
184+
// TODO(user): escape args? (they are currently file names, which shouldn't contain quotes)
185+
engine.getContext().setWriter(outputWriter);
186+
engine.eval(String.format(python, PATH_TO_BUCK_PY, Joiner.on("\",\"").join(args)));
187+
188+
} catch (ScriptException e) {
189+
throw Throwables.propagate(e);
190+
} finally {
191+
try {
192+
closer.close();
193+
} catch (IOException e) {
194+
Throwables.propagate(e);
195+
}
196+
}
197+
}
198+
}
199+
146200
/**
147201
* @param rootPath Absolute path to the root of the project. buck.py uses this to determine the
148202
* base path of the targets in the build file that it is parsing.
@@ -154,11 +208,27 @@ public static List<Map<String, Object>> getAllRules(
154208
Optional<String> buildFile,
155209
Iterable<String> includes,
156210
Ansi ansi) throws IOException {
157-
// A list of the build rules parsed from the build file.
211+
212+
// Run the build file in a background thread.
213+
final ImmutableList<String> args = buildArgs(rootPath, buildFile, includes);
214+
final PipedReader outputReader = new PipedReader();
215+
BuildFileRunner runner = new BuildFileRunner(args, outputReader);
216+
executor.execute(runner);
217+
218+
// Stream build rules from python.
219+
BuildFileToJsonParser parser = new BuildFileToJsonParser(outputReader);
158220
List<Map<String, Object>> rules = Lists.newArrayList();
221+
Map<String, Object> value;
222+
while ((value = parser.next()) != null) {
223+
rules.add(value);
224+
}
225+
226+
return rules;
227+
}
159228

229+
private static ImmutableList<String> buildArgs(String rootPath, Optional<String> buildFile, Iterable<String> includes) {
160230
// Create a process to run buck.py and read its stdout.
161-
List<String> args = Lists.newArrayList("python", PATH_TO_BUCK_PY, "--project_root", rootPath);
231+
List<String> args = Lists.newArrayList("buck.py", "--project_root", rootPath);
162232

163233
// Add the --include flags.
164234
for (String include : includes) {
@@ -170,42 +240,6 @@ public static List<Map<String, Object>> getAllRules(
170240
if (buildFile.isPresent()) {
171241
args.add(buildFile.get());
172242
}
173-
174-
ProcessBuilder processBuilder = new ProcessBuilder(args);
175-
Process process = processBuilder.start();
176-
BuildFileToJsonParser parser = new BuildFileToJsonParser(process.getInputStream());
177-
178-
// If the build file parses cleanly, then nothing should be written to standard error. We make
179-
// sure to consume stderr, just as we do in ShellCommand, to avoid potential deadlock.
180-
InputStreamConsumer stdErr = new InputStreamConsumer(
181-
process.getErrorStream(),
182-
System.err,
183-
true /* shouldPrintStdErr */,
184-
ansi);
185-
Thread stdErrConsumer = new Thread(stdErr);
186-
stdErrConsumer.start();
187-
188-
// Read parsed JSON objects from the stream as they become available.
189-
Map<String, Object> value = null;
190-
while ((value = parser.next()) != null) {
191-
rules.add(value);
192-
}
193-
194-
// Make sure the process exits with zero. If not, throw an exception. The error was likely
195-
// printed to stderr by the InputStreamConsumer.
196-
try {
197-
int exitCode = process.waitFor();
198-
if (exitCode != 0) {
199-
if (buildFile.isPresent()) {
200-
throw new RuntimeException("Parsing " + buildFile.get() + " did not exit cleanly");
201-
} else {
202-
throw new RuntimeException("Error parsing build files");
203-
}
204-
}
205-
} catch (InterruptedException e) {
206-
throw Throwables.propagate(e);
207-
}
208-
209-
return rules;
243+
return ImmutableList.copyOf(args);
210244
}
211245
}

src/com/facebook/buck/parser/buck.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,35 @@
1-
import argparse
1+
from __future__ import with_statement
2+
3+
import optparse
24
import functools
3-
import json
45
import re
56
import os
67
import os.path
78
import posixpath
89

10+
try:
11+
from com.xhaus.jyson import JysonCodec as json # jython embedded in buck
12+
except ImportError:
13+
import json # python test case
14+
15+
16+
# TODO(user): upgrade to a jython including os.relpath
17+
def relpath(path, start=posixpath.curdir):
18+
"""
19+
Return a relative filepath to path from the current directory or an optional start point.
20+
"""
21+
if not path:
22+
raise ValueError("no path specified")
23+
start_list = posixpath.abspath(start).split(posixpath.sep)
24+
path_list = posixpath.abspath(path).split(posixpath.sep)
25+
# Work out how much of the filepath is shared by start and path.
26+
common = len(posixpath.commonprefix([start_list, path_list]))
27+
rel_list = [posixpath.pardir] * (len(start_list) - common) + path_list[common:]
28+
if not rel_list:
29+
return posixpath.curdir
30+
return posixpath.join(*rel_list)
31+
32+
933
# When BUILD files are executed, the functions in this file tagged with
1034
# @provide_for_build will be provided in the BUILD file's local symbol table.
1135
#
@@ -103,7 +127,7 @@ def check_path(path):
103127
for root, dirs, files in os.walk(search_base):
104128
if len(files) == 0:
105129
continue
106-
relative_root = os.path.relpath(root, search_base)
130+
relative_root = relpath(root, search_base)
107131
# The regexes generated by glob_pattern_to_regex_string don't
108132
# expect a leading './'
109133
if relative_root == '.':
@@ -588,19 +612,18 @@ def parse_git_ignore(gitignore_data):
588612
# parser. That means that printing out other information for debugging purposes will likely break
589613
# the JSON parsing, so be careful!
590614
def main():
591-
parser = argparse.ArgumentParser()
592-
parser.add_argument('--project_root')
593-
parser.add_argument('--include', action='append')
594-
parser.add_argument('build_files', nargs='*')
595-
args = parser.parse_args()
615+
parser = optparse.OptionParser()
616+
parser.add_option('--project_root', action='store', type='string', dest='project_root')
617+
parser.add_option('--include', action='append', dest='include')
618+
(options, args) = parser.parse_args()
596619

597-
project_root = args.project_root
620+
project_root = options.project_root
598621
len_suffix = -len('/' + BUILD_RULES_FILE_NAME)
599622

600623
build_files = None
601-
if args.build_files:
624+
if args:
602625
# The user has specified which build files to parse.
603-
build_files = args.build_files
626+
build_files = args
604627
else:
605628
# Find all of the build files in the project root. Symlinks will not be traversed.
606629
# Search must be done top-down so that directory filtering works as desired.
@@ -633,14 +656,14 @@ def main():
633656
# or the files in includes through include_defs() don't pollute the namespace for
634657
# subsequent build files.
635658
build_env = {}
636-
relative_path_to_build_file = os.path.relpath(build_file, project_root)
659+
relative_path_to_build_file = relpath(build_file, project_root)
637660
build_env['BASE'] = relative_path_to_build_file[:len_suffix]
638661
build_env['BUILD_FILE_DIRECTORY'] = os.path.dirname(build_file)
639662
build_env['PROJECT_ROOT'] = project_root
640663
build_env['BUILD_FILE_SYMBOL_TABLE'] = make_build_file_symbol_table(build_env)
641664

642665
# If there are any default includes, evaluate those first to populate the build_env.
643-
includes = args.include or []
666+
includes = options.include or []
644667
for include in includes:
645668
include_defs(include, build_env)
646669
execfile(os.path.join(project_root, build_file), build_env['BUILD_FILE_SYMBOL_TABLE'])

0 commit comments

Comments
 (0)