diff options
author | Simon Sapin <simon.sapin@exyr.org> | 2019-09-27 06:37:54 +0200 |
---|---|---|
committer | Simon Sapin <simon.sapin@exyr.org> | 2019-09-27 13:53:19 +0200 |
commit | 5c60023cb8ad6f07927bd53f30c90873d07b300f (patch) | |
tree | 9ea739dbeb622a918b7636558f7ed81ff6f4bb49 | |
parent | 049527872e6dfadf3f69f0f9fa6fffee520a6f7b (diff) | |
download | servo-5c60023cb8ad6f07927bd53f30c90873d07b300f.tar.gz servo-5c60023cb8ad6f07927bd53f30c90873d07b300f.zip |
WebIDL codegen: Replace cmake with a single Python script
When playing around with Cargo’s new timing visualization:
https://internals.rust-lang.org/t/exploring-crate-graph-build-times-with-cargo-build-ztimings/10975/21
… I was surprised to see the `script` crate’s build script take 76 seconds.
I did not expect WebIDL bindings generation to be *that* computationally
intensive.
It turns out almost all of this time is overhead. The build script uses CMake
to generate bindings for each WebIDL file in parallel, but that causes a lot
of work to be repeated 366 times:
* Starting up a Python VM
* Importing (parts of) the Python standard library
* Importing ~16k lines of our Python code
* Recompiling the latter to bytecode, since we used `python -B` to disable
writing `.pyc` file
* Deserializing with `cPickle` and recreating in memory the results
of parsing all WebIDL files
----
This commit remove the use of CMake and cPickle for the `script` crate.
Instead, all WebIDL bindings generation is done sequentially
in a single Python process. This takes 2 to 3 seconds.
-rw-r--r-- | components/script/CMakeLists.txt | 104 | ||||
-rw-r--r-- | components/script/build.rs | 58 | ||||
-rw-r--r-- | components/script/dom/bindings/codegen/BindingGen.py | 54 | ||||
-rw-r--r-- | components/script/dom/bindings/codegen/CodegenRust.py | 26 | ||||
-rw-r--r-- | components/script/dom/bindings/codegen/GlobalGen.py | 143 | ||||
-rw-r--r-- | components/script/dom/bindings/codegen/pythonpath.py | 61 | ||||
-rw-r--r-- | components/script/dom/bindings/codegen/run.py | 119 | ||||
-rw-r--r-- | components/script/dom/mod.rs | 3 | ||||
-rw-r--r-- | components/style/build.rs | 66 |
9 files changed, 177 insertions, 457 deletions
diff --git a/components/script/CMakeLists.txt b/components/script/CMakeLists.txt deleted file mode 100644 index 4ad45327d29..00000000000 --- a/components/script/CMakeLists.txt +++ /dev/null @@ -1,104 +0,0 @@ -project(script LANGUAGES) -cmake_minimum_required(VERSION 2.6) - -set(DUMMY ${CMAKE_BUILD_TYPE}) - -FUNCTION(PREPEND var prefix) - SET(listVar "") - FOREACH(f ${ARGN}) - LIST(APPEND listVar "${prefix}/${f}") - ENDFOREACH(f) - SET(${var} "${listVar}" PARENT_SCOPE) -ENDFUNCTION(PREPEND) - -set(bindings_src ${PROJECT_SOURCE_DIR}/dom/bindings/codegen) -set(webidls_src ${PROJECT_SOURCE_DIR}/dom/webidls) - -# Without Bindings/* stuff, since we install that separately below -set(globalgen_base_src - PrototypeList.rs - RegisterBindings.rs - InterfaceObjectMap.rs - InterfaceTypes.rs - InheritTypes.rs - UnionTypes.rs - ) - -set(globalgen_src - ${globalgen_base_src} - Bindings/mod.rs - ) - -file(GLOB_RECURSE webidls ${webidls_src}/*.webidl) -string(REGEX REPLACE ";" "\n" webidl_filelist "${webidls}") -file(WRITE "${PROJECT_BINARY_DIR}/webidls.list" "${webidl_filelist}") -string(REGEX REPLACE "\\.webidl(;|$)" "\\1" bindings "${webidls}") -string(REGEX REPLACE "(^|;)${webidls_src}/" "\\1" bindings "${bindings}") - -set(globalgen_deps - ${bindings_src}/GlobalGen.py - ${bindings_src}/Bindings.conf - ${bindings_src}/Configuration.py - ${bindings_src}/CodegenRust.py - ${bindings_src}/parser/WebIDL.py - ) -set(bindinggen_deps - ${globalgen_deps} - ${bindings_src}/BindingGen.py - ) - -add_custom_command( - OUTPUT Bindings - COMMAND ${CMAKE_COMMAND} -E make_directory Bindings - ) -add_custom_command( - OUTPUT _cache - COMMAND ${CMAKE_COMMAND} -E make_directory _cache - ) - -# Specify python 2 as required -find_package( PythonInterp 2 REQUIRED ) - -add_custom_command( - OUTPUT ParserResults.pkl - COMMAND ${PYTHON_EXECUTABLE} -B ${bindings_src}/pythonpath.py -I ${bindings_src}/parser -I ${bindings_src}/ply - ${bindings_src}/GlobalGen.py - --cachedir=_cache - --filelist=webidls.list - ${bindings_src}/Bindings.conf - . - ${PROJECT_SOURCE_DIR} - ${PROJECT_BINARY_DIR}/../css-properties.json - ${PROJECT_SOURCE_DIR}/../../target/doc/servo - DEPENDS Bindings _cache ${globalgen_deps} ${webidls} ${PROJECT_BINARY_DIR}/../css-properties.json - VERBATIM - ) - -# We need an intermediate custom target for this, due to this misfeature: -# > If any dependency is an OUTPUT of another custom command in the same -# > directory CMake automatically brings the other custom command into the -# > target in which this command is built. -# So, depending directly on ParserResults.pkl from the add_custom_command -# below would cause GlobalGen.py to be executed each time. -add_custom_target(ParserResults ALL DEPENDS ParserResults.pkl) -add_custom_target(generate-bindings ALL) - -foreach(binding IN LISTS bindings) - add_custom_command( - OUTPUT Bindings/${binding}Binding.rs - COMMAND ${PYTHON_EXECUTABLE} -B ${bindings_src}/pythonpath.py -I ${bindings_src}/parser -I ${bindings_src}/ply - ${bindings_src}/BindingGen.py - ${bindings_src}/Bindings.conf - . - Bindings/${binding}Binding - ${webidls_src}/${binding}.webidl - DEPENDS Bindings ${bindinggen_deps} ${webidls} ParserResults - VERBATIM - ) - add_custom_target(${binding} DEPENDS Bindings/${binding}Binding.rs) - add_dependencies(generate-bindings ${binding}) -endforeach() - -PREPEND(globalgen_out ${CMAKE_BINARY_DIR}/ ${globalgen_base_src}) -install(FILES ${globalgen_out} DESTINATION .) -install(DIRECTORY ${CMAKE_BINARY_DIR}/Bindings/ DESTINATION Bindings) diff --git a/components/script/build.rs b/components/script/build.rs index 1e98ab5382c..2699c533c6a 100644 --- a/components/script/build.rs +++ b/components/script/build.rs @@ -9,7 +9,7 @@ use std::fmt; use std::fs::File; use std::io::Write; use std::path::PathBuf; -use std::str; +use std::process::Command; use std::time::Instant; fn main() { @@ -17,30 +17,22 @@ fn main() { let style_out_dir = PathBuf::from(env::var_os("DEP_SERVO_STYLE_CRATE_OUT_DIR").unwrap()); let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); - let json = "css-properties.json"; - std::fs::copy(style_out_dir.join(json), out_dir.join(json)).unwrap(); + let target_dir = PathBuf::from(env::var_os("CARGO_TARGET_DIR").unwrap()); - // This must use the Ninja generator -- it's the only one that - // parallelizes cmake's output properly. (Cmake generates - // separate makefiles, each of which try to build - // ParserResults.pkl, and then stomp on eachother.) - let mut build = cmake::Config::new("."); - - let target = env::var("TARGET").unwrap(); - if target.contains("windows-msvc") { - // We must use Ninja on Windows for this -- msbuild is painfully slow, - // and ninja is easier to install than make. - build.generator("Ninja"); + let status = Command::new(find_python()) + .arg("dom/bindings/codegen/run.py") + .arg(style_out_dir.join("css-properties.json")) + .arg(&out_dir) + .arg(target_dir.join("doc").join("servo")) + .status() + .unwrap(); + if !status.success() { + std::process::exit(1) } - build.build(); - - println!( - "Binding generation completed in {}s", - start.elapsed().as_secs() - ); + println!("Binding generation completed in {:?}", start.elapsed()); - let json = out_dir.join("build").join("InterfaceObjectMapData.json"); + let json = out_dir.join("InterfaceObjectMapData.json"); let json: Value = serde_json::from_reader(File::open(&json).unwrap()).unwrap(); let mut map = phf_codegen::Map::new(); for (key, value) in json.as_object().unwrap() { @@ -74,3 +66,27 @@ impl<'a> phf_shared::PhfHash for Bytes<'a> { self.0.as_bytes().phf_hash(hasher) } } + +fn find_python() -> String { + env::var("PYTHON").ok().unwrap_or_else(|| { + let candidates = if cfg!(windows) { + ["python2.7.exe", "python27.exe", "python.exe"] + } else { + ["python2.7", "python2", "python"] + }; + for &name in &candidates { + if Command::new(name) + .arg("--version") + .output() + .ok() + .map_or(false, |out| out.status.success()) + { + return name.to_owned(); + } + } + panic!( + "Can't find python (tried {})! Try fixing PATH or setting the PYTHON env var", + candidates.join(", ") + ) + }) +} diff --git a/components/script/dom/bindings/codegen/BindingGen.py b/components/script/dom/bindings/codegen/BindingGen.py deleted file mode 100644 index 63cc68e46e5..00000000000 --- a/components/script/dom/bindings/codegen/BindingGen.py +++ /dev/null @@ -1,54 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import sys -import os -sys.path.append(os.path.join(".", "parser")) -sys.path.append(os.path.join(".", "ply")) -import cPickle -from Configuration import Configuration -from CodegenRust import CGBindingRoot, replaceFileIfChanged - - -def generate_binding_rs(config, outputprefix, webidlfile): - """ - |config| Is the configuration object. - |outputprefix| is a prefix to use for the header guards and filename. - """ - - filename = outputprefix + ".rs" - module = CGBindingRoot(config, outputprefix, webidlfile).define() - if not module: - print "Skipping empty module: %s" % (filename) - elif replaceFileIfChanged(filename, module): - print "Generating binding implementation: %s" % (filename) - - -def main(): - # Parse arguments. - from optparse import OptionParser - usagestring = "usage: %prog configFile outputdir outputPrefix webIDLFile" - o = OptionParser(usage=usagestring) - (options, args) = o.parse_args() - - if len(args) != 4: - o.error(usagestring) - configFile = os.path.normpath(args[0]) - outputdir = args[1] - outputPrefix = args[2] - webIDLFile = os.path.normpath(args[3]) - - # Load the parsing results - resultsPath = os.path.join(outputdir, 'ParserResults.pkl') - with open(resultsPath, 'rb') as f: - parserData = cPickle.load(f) - - # Create the configuration data. - config = Configuration(configFile, parserData) - - # Generate the prototype classes. - generate_binding_rs(config, outputPrefix, webIDLFile) - -if __name__ == '__main__': - main() diff --git a/components/script/dom/bindings/codegen/CodegenRust.py b/components/script/dom/bindings/codegen/CodegenRust.py index 3cd4078814b..c02499f222f 100644 --- a/components/script/dom/bindings/codegen/CodegenRust.py +++ b/components/script/dom/bindings/codegen/CodegenRust.py @@ -54,32 +54,6 @@ RUST_KEYWORDS = {"abstract", "alignof", "as", "become", "box", "break", "const", "use", "virtual", "where", "while", "yield"} -def replaceFileIfChanged(filename, newContents): - """ - Read a copy of the old file, so that we don't touch it if it hasn't changed. - Returns True if the file was updated, false otherwise. - """ - # XXXjdm This doesn't play well with make right now. - # Force the file to always be updated, or else changing CodegenRust.py - # will cause many autogenerated bindings to be regenerated perpetually - # until the result is actually different. - - # oldFileContents = "" - # try: - # with open(filename, 'rb') as oldFile: - # oldFileContents = ''.join(oldFile.readlines()) - # except: - # pass - - # if newContents == oldFileContents: - # return False - - with open(filename, 'wb') as f: - f.write(newContents) - - return True - - def toStringBool(arg): return str(not not arg).lower() diff --git a/components/script/dom/bindings/codegen/GlobalGen.py b/components/script/dom/bindings/codegen/GlobalGen.py deleted file mode 100644 index 1850a41d5f3..00000000000 --- a/components/script/dom/bindings/codegen/GlobalGen.py +++ /dev/null @@ -1,143 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - -# We do one global pass over all the WebIDL to generate our prototype enum -# and generate information for subsequent phases. - -import sys -import os -import json -sys.path.append(os.path.join(".", "parser")) -sys.path.append(os.path.join(".", "ply")) -import WebIDL -import cPickle -from Configuration import Configuration -from CodegenRust import GlobalGenRoots, replaceFileIfChanged - - -def generate_file(config, name, filename): - root = getattr(GlobalGenRoots, name)(config) - code = root.define() - - if replaceFileIfChanged(filename, code): - print "Generating %s" % (filename) - else: - print "%s hasn't changed - not touching it" % (filename) - - -def main(): - # Parse arguments. - from optparse import OptionParser - usageString = "usage: %prog [options] configFile outputdir webidldir cssProperties.json docServoDir [files]" - o = OptionParser(usage=usageString) - o.add_option("--cachedir", dest='cachedir', default=None, - help="Directory in which to cache lex/parse tables.") - o.add_option("--filelist", dest='filelist', default=None, - help="A file containing the list (one per line) of webidl files to process.") - (options, args) = o.parse_args() - - if len(args) < 2: - o.error(usageString) - - configFile = args[0] - outputdir = args[1] - baseDir = args[2] - css_properties_json = args[3] - doc_servo = args[4] - if options.filelist is not None: - fileList = [l.strip() for l in open(options.filelist).xreadlines()] - else: - fileList = args[3:] - - # Parse the WebIDL. - parser = WebIDL.Parser(options.cachedir) - for filename in fileList: - fullPath = os.path.normpath(os.path.join(baseDir, filename)) - with open(fullPath, 'rb') as f: - lines = f.readlines() - parser.parse(''.join(lines), fullPath) - - add_css_properties_attributes(fileList, css_properties_json, parser) - - parserResults = parser.finish() - - # Write the parser results out to a pickle. - resultsPath = os.path.join(outputdir, 'ParserResults.pkl') - with open(resultsPath, 'wb') as resultsFile: - cPickle.dump(parserResults, resultsFile, -1) - - # Load the configuration. - config = Configuration(configFile, parserResults) - - to_generate = [ - ('PrototypeList', 'PrototypeList.rs'), - ('RegisterBindings', 'RegisterBindings.rs'), - ('InterfaceObjectMap', 'InterfaceObjectMap.rs'), - ('InterfaceObjectMapData', 'InterfaceObjectMapData.json'), - ('InterfaceTypes', 'InterfaceTypes.rs'), - ('InheritTypes', 'InheritTypes.rs'), - ('Bindings', os.path.join('Bindings', 'mod.rs')), - ('UnionTypes', 'UnionTypes.rs'), - ] - - for name, filename in to_generate: - generate_file(config, name, os.path.join(outputdir, filename)) - - generate_file(config, 'SupportedDomApis', os.path.join(doc_servo, 'apis.html')) - - -def add_css_properties_attributes(webidl_files, css_properties_json, parser): - for filename in webidl_files: - if os.path.basename(filename) == "CSSStyleDeclaration.webidl": - break - else: - return - - css_properties = json.load(open(css_properties_json, "rb")) - idl = "partial interface CSSStyleDeclaration {\n%s\n};\n" % "\n".join( - " [%sCEReactions, SetterThrows] attribute [TreatNullAs=EmptyString] DOMString %s;" % ( - ('Pref="%s", ' % data["pref"] if data["pref"] else ""), - attribute_name - ) - for (kind, properties_list) in sorted(css_properties.items()) - for (property_name, data) in sorted(properties_list.items()) - for attribute_name in attribute_names(property_name) - ) - parser.parse(idl.encode("utf-8"), "CSSStyleDeclaration_generated.webidl") - - -def attribute_names(property_name): - # https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-dashed-attribute - if property_name != "float": - yield property_name - else: - yield "_float" - - # https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-camel-cased-attribute - if "-" in property_name: - yield "".join(camel_case(property_name)) - - # https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-webkit-cased-attribute - if property_name.startswith("-webkit-"): - yield "".join(camel_case(property_name), True) - - -# https://drafts.csswg.org/cssom/#css-property-to-idl-attribute -def camel_case(chars, webkit_prefixed=False): - if webkit_prefixed: - chars = chars[1:] - next_is_uppercase = False - for c in chars: - if c == '-': - next_is_uppercase = True - elif next_is_uppercase: - next_is_uppercase = False - # Should be ASCII-uppercase, but all non-custom CSS property names are within ASCII - yield c.upper() - else: - yield c - - -if __name__ == '__main__': - main() diff --git a/components/script/dom/bindings/codegen/pythonpath.py b/components/script/dom/bindings/codegen/pythonpath.py deleted file mode 100644 index 67739c3625f..00000000000 --- a/components/script/dom/bindings/codegen/pythonpath.py +++ /dev/null @@ -1,61 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - -""" -Run a python script, adding extra directories to the python path. -""" - - -def main(args): - def usage(): - print >>sys.stderr, "pythonpath.py -I directory script.py [args...]" - sys.exit(150) - - paths = [] - - while True: - try: - arg = args[0] - except IndexError: - usage() - - if arg == '-I': - args.pop(0) - try: - path = args.pop(0) - except IndexError: - usage() - - paths.append(os.path.abspath(path)) - continue - - if arg.startswith('-I'): - paths.append(os.path.abspath(args.pop(0)[2:])) - continue - - if arg.startswith('-D'): - os.chdir(args.pop(0)[2:]) - continue - - break - - script = args[0] - - sys.path[0:0] = [os.path.abspath(os.path.dirname(script))] + paths - sys.argv = args - sys.argc = len(args) - - frozenglobals['__name__'] = '__main__' - frozenglobals['__file__'] = script - - execfile(script, frozenglobals) - -# Freeze scope here ... why this makes things work I have no idea ... -frozenglobals = globals() - -import sys -import os - -if __name__ == '__main__': - main(sys.argv[1:]) diff --git a/components/script/dom/bindings/codegen/run.py b/components/script/dom/bindings/codegen/run.py new file mode 100644 index 00000000000..fd43ac32bb7 --- /dev/null +++ b/components/script/dom/bindings/codegen/run.py @@ -0,0 +1,119 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import os +import sys +import json + + +def main(): + os.chdir(os.path.join(os.path.dirname(__file__))) + sys.path[0:0] = ["./parser", "./ply"] + + css_properties_json, out_dir, doc_servo = sys.argv[1:] + webidls_dir = "../../webidls" + config_file = "Bindings.conf" + + import WebIDL + from Configuration import Configuration + from CodegenRust import CGBindingRoot + + parser = WebIDL.Parser(make_dir(out_dir, "cache")) + webidls = [name for name in os.listdir(webidls_dir) if name.endswith(".webidl")] + for webidl in webidls: + filename = os.path.join(webidls_dir, webidl) + with open(filename, "rb") as f: + parser.parse(f.read(), filename) + + add_css_properties_attributes(css_properties_json, parser) + parser_results = parser.finish() + config = Configuration(config_file, parser_results) + make_dir(out_dir, "Bindings") + + for name, filename in [ + ("PrototypeList", "PrototypeList.rs"), + ("RegisterBindings", "RegisterBindings.rs"), + ("InterfaceObjectMap", "InterfaceObjectMap.rs"), + ("InterfaceObjectMapData", "InterfaceObjectMapData.json"), + ("InterfaceTypes", "InterfaceTypes.rs"), + ("InheritTypes", "InheritTypes.rs"), + ("Bindings", "Bindings/mod.rs"), + ("UnionTypes", "UnionTypes.rs"), + ]: + generate(config, name, os.path.join(out_dir, filename)) + make_dir(doc_servo) + generate(config, "SupportedDomApis", os.path.join(doc_servo, "apis.html")) + + for webidl in webidls: + filename = os.path.join(webidls_dir, webidl) + prefix = "Bindings/%sBinding" % webidl[:-len(".webidl")] + module = CGBindingRoot(config, prefix, filename).define() + if module: + with open(os.path.join(out_dir, prefix + ".rs"), "wb") as f: + f.write(module) + + +def make_dir(out_dir, nested=""): + path = os.path.join(out_dir, nested) + if not os.path.exists(path): + os.makedirs(path) + return path + + +def generate(config, name, filename): + from CodegenRust import GlobalGenRoots + root = getattr(GlobalGenRoots, name)(config) + code = root.define() + with open(filename, "wb") as f: + f.write(code) + + +def add_css_properties_attributes(css_properties_json, parser): + css_properties = json.load(open(css_properties_json, "rb")) + idl = "partial interface CSSStyleDeclaration {\n%s\n};\n" % "\n".join( + " [%sCEReactions, SetterThrows] attribute [TreatNullAs=EmptyString] DOMString %s;" % ( + ('Pref="%s", ' % data["pref"] if data["pref"] else ""), + attribute_name + ) + for (kind, properties_list) in sorted(css_properties.items()) + for (property_name, data) in sorted(properties_list.items()) + for attribute_name in attribute_names(property_name) + ) + parser.parse(idl.encode("utf-8"), "CSSStyleDeclaration_generated.webidl") + + +def attribute_names(property_name): + # https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-dashed-attribute + if property_name != "float": + yield property_name + else: + yield "_float" + + # https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-camel-cased-attribute + if "-" in property_name: + yield "".join(camel_case(property_name)) + + # https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-webkit-cased-attribute + if property_name.startswith("-webkit-"): + yield "".join(camel_case(property_name), True) + + +# https://drafts.csswg.org/cssom/#css-property-to-idl-attribute +def camel_case(chars, webkit_prefixed=False): + if webkit_prefixed: + chars = chars[1:] + next_is_uppercase = False + for c in chars: + if c == '-': + next_is_uppercase = True + elif next_is_uppercase: + next_is_uppercase = False + # Should be ASCII-uppercase, but all non-custom CSS property names are within ASCII + yield c.upper() + else: + yield c + + +if __name__ == "__main__": + main() diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index d698fcc709f..5a40469f41a 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -206,10 +206,7 @@ pub mod macros; pub mod types { - #[cfg(not(target_env = "msvc"))] include!(concat!(env!("OUT_DIR"), "/InterfaceTypes.rs")); - #[cfg(target_env = "msvc")] - include!(concat!(env!("OUT_DIR"), "/build/InterfaceTypes.rs")); } pub mod abstractworker; diff --git a/components/style/build.rs b/components/style/build.rs index 83d996da1be..4477e648ac9 100644 --- a/components/style/build.rs +++ b/components/style/build.rs @@ -27,52 +27,28 @@ mod build_gecko { pub fn generate() {} } -#[cfg(windows)] -fn find_python() -> String { - if Command::new("python2.7.exe") - .arg("--version") - .output() - .is_ok() - { - return "python2.7.exe".to_owned(); - } - - if Command::new("python27.exe") - .arg("--version") - .output() - .is_ok() - { - return "python27.exe".to_owned(); - } - - if Command::new("python.exe").arg("--version").output().is_ok() { - return "python.exe".to_owned(); - } - - panic!(concat!( - "Can't find python (tried python2.7.exe, python27.exe, and python.exe)! ", - "Try fixing PATH or setting the PYTHON env var" - )); -} - -#[cfg(not(windows))] -fn find_python() -> String { - if Command::new("python2.7") - .arg("--version") - .output() - .unwrap() - .status - .success() - { - "python2.7" - } else { - "python" - } - .to_owned() -} - lazy_static! { - pub static ref PYTHON: String = env::var("PYTHON").ok().unwrap_or_else(find_python); + pub static ref PYTHON: String = env::var("PYTHON").ok().unwrap_or_else(|| { + let candidates = if cfg!(windows) { + ["python2.7.exe", "python27.exe", "python.exe"] + } else { + ["python2.7", "python2", "python"] + }; + for &name in &candidates { + if Command::new(name) + .arg("--version") + .output() + .ok() + .map_or(false, |out| out.status.success()) + { + return name.to_owned(); + } + } + panic!( + "Can't find python (tried {})! Try fixing PATH or setting the PYTHON env var", + candidates.join(", ") + ) + }); } fn generate_properties(engine: &str) { |