diff --git a/README.md b/README.md index 3567997667..1ae4050877 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,15 @@ It works by building the project with a fake toolchain, which simply and filters It is reasonably fast, taking ~10 seconds to generate a configuration file for the Linux kernel. +## Installation +Add ```NeoBundle 'rdnetto/YCM-Generator'``` to your vimrc (or the equivalent for your plugin manager). + +Alternatively, Arch Linux users can install YCM-Generator using the (unofficial) [AUR package](https://aur4.archlinux.org/packages/ycm-generator-git/). + ## Usage Run ```./config_gen.py PROJECT_DIRECTORY```, where ```PROJECT_DIRECTORY``` is the root directory of your project's build system (i.e. the one containing the root Makefile, etc.) -YCM-Generator can also be used as a Vim plugin. Once installed with Vundle/NeoBundle/etc., use the ```:YcmGenerateConfig``` or ```:CCGenerateConfig``` command to generate a config file for the current directory. This command accepts the same arguments as ```./config_gen.py```, but does not require the project directory to be specified (it defaults to the current working directory). +You can also invoke it from within Vim using the ```:YcmGenerateConfig``` or ```:CCGenerateConfig``` commands to generate a config file for the current directory. These commands accept the same arguments as ```./config_gen.py```, but do not require the project directory to be specified (it defaults to the current working directory). ## Requirements and Limitations * Requirements: @@ -19,27 +24,41 @@ YCM-Generator can also be used as a Vim plugin. Once installed with Vundle/NeoBu + Clang * Supported build systems: - + Make - + CMake - + Autotools + + make + + cmake + + qmake + + autotools Your build system should support specifying the compiler through the ```CC```/```CXX``` environment variables, or not use an absolute path to the compiler. Some flags present in the resulting configuration file may be mutually exclusive with each other - reading the generated file prior to use is recommended. -# Documentation & Support +## Documentation & Support * run ```./config_gen.py --help``` to see the complete list of supported options. * if you receive the error message ```ERROR: No commands were logged to the build logs```, try using the ```--verbose``` flag to see any error messages + + some build systems require certain environment variables to be set. Note that these will *not* be used by YCM-Generator by default, unless `--preserve-environment` is used + if you open an issue regarding this error message, please include the output when running with ```--verbose``` and a link to the project repo (if possible) ## Development Patches are welcome. Please submit pull requests against the ```develop``` branch. ### Windows support -The script is currently supported under Unices (Linux, BSD, OS X) only. +The script is currently supported under Unices (Linux, NixOS[1], BSD, OS X) only. Implementing Windows support consists of porting the contents of ```fake-toolchain/Unix```. If you are interested in implementing/testing this, please open a pull request. +[1] May require `--preserve-environment` - see [#19](https://github.com/rdnetto/YCM-Generator/issues/19) + +### Test Cases +The following projects are used for testing: + +| Project | Build system | Notes | +| ------------------------------------------------------------------------- | ----------------- | ------ | +| [Linux kernel](https://git.kernel.org) | Kbuild (Make) | | +| [Vim-qt](https://rdnetto@bitbucket.org/equalsraf/vim-qt.git) | Autotools | | +| [Clementine](https://github.com/clementine-player/Clementine.git) | Cmake | | +| [ExtPlane](https://github.com/vranki/ExtPlane.git) | Qmake | Should be tested with both versions of Qt. | + ## License YCM-Generator is published under the GNU GPLv3. diff --git a/config_gen.py b/config_gen.py index da0d3d7203..7d78df3b35 100755 --- a/config_gen.py +++ b/config_gen.py @@ -12,6 +12,7 @@ import tempfile import time import subprocess +import glob # Default flags for make @@ -30,6 +31,7 @@ def main(): parser.add_argument("-o", "--output", help="Save the config file as OUTPUT. Default: .ycm_extra_conf.py, or .color_coded if --format=cc.") parser.add_argument("-x", "--language", choices=["c", "c++"], help="Only output flags for the given language. This defaults to whichever language has its compiler invoked the most.") parser.add_argument("--out-of-tree", action="store_true", help="Build autotools projects out-of-tree. This is a no-op for other project types.") + parser.add_argument("--qt-version", choices=["4", "5"], default="5", help="Use the given Qt version for qmake. (Default: 5)") parser.add_argument("-e", "--preserve-environment", action="store_true", help="Pass environment variables to build processes.") parser.add_argument("PROJECT_DIR", help="The root directory of the project.") args = vars(parser.parse_args()) @@ -122,7 +124,7 @@ def main(): print("Created {} config file with {} {} flags".format(output_format.upper(), len(flags), lang.upper())) -def fake_build(project_dir, c_build_log_path, cxx_build_log_path, verbose, make_cmd, cc, cxx, out_of_tree, configure_opts, make_flags, preserve_environment): +def fake_build(project_dir, c_build_log_path, cxx_build_log_path, verbose, make_cmd, cc, cxx, out_of_tree, configure_opts, make_flags, preserve_environment, qt_version): '''Builds the project using the fake toolchain, to collect the compiler flags. project_dir: the directory containing the source files @@ -135,6 +137,7 @@ def fake_build(project_dir, c_build_log_path, cxx_build_log_path, verbose, make_ configure_opts: additional flags for configure stage make_flags: additional flags for make preserve_environment: pass environment variables to build processes + qt_version: The Qt version to use when building with qmake. ''' # TODO: add Windows support @@ -167,6 +170,9 @@ def fake_build(project_dir, c_build_log_path, cxx_build_log_path, verbose, make_ # depend upon the existence of various output files make_args = [make_cmd] + make_flags + # Used for the qmake build system below + pro_files = glob.glob(os.path.join(project_dir, "*.pro")) + # sanity check - make sure the toolchain is available assert os.path.exists(fake_path), "Could not find toolchain at '{}'".format(fake_path) @@ -177,11 +183,22 @@ def run(cmd, *args, **kwargs): # execute the build system if(os.path.exists(os.path.join(project_dir, "CMakeLists.txt"))): - # Cmake + # cmake # run cmake in a temporary directory, then compile the project as usual build_dir = tempfile.mkdtemp() proc_opts["cwd"] = build_dir + # if the project was built in-tree, we need to hide the cache file so that cmake + # populates the build dir instead of just re-generating the existing files + cache_path = os.path.join(project_dir, "CMakeCache.txt") + + if(os.path.exists(cache_path)): + fd, cache_tmp = tempfile.mkstemp() + os.close(fd) + shutil.move(cache_path, cache_tmp) + else: + cache_tmp = None + print("Running cmake in '{}'...".format(build_dir)) run(["cmake", project_dir] + configure_opts, env=env_config, **proc_opts) @@ -192,8 +209,11 @@ def run(cmd, *args, **kwargs): print("") shutil.rmtree(build_dir) + if(cache_tmp): + shutil.move(cache_tmp, cache_path) + elif(os.path.exists(os.path.join(project_dir, "configure"))): - # Autotools + # autotools # perform build in-tree, since not all projects handle out-of-tree builds correctly if(out_of_tree): @@ -216,8 +236,33 @@ def run(cmd, *args, **kwargs): else: run([make_cmd, "maintainer-clean"], env=env, **proc_opts) + elif(pro_files): + # qmake + # make sure there is only one .pro file + if len(pro_files) != 1: + print("ERROR: Found {} .pro files (expected one): {}.".format( + len(pro_files), ', '.join(pro_files))) + sys.exit(1) + + # run qmake in a temporary directory, then compile the project as usual + build_dir = tempfile.mkdtemp() + proc_opts["cwd"] = build_dir + env_config["QT_SELECT"] = qt_version + env_config["QMAKESPEC"] = "unsupported/linux-clang" if qt_version == "4" else "linux-clang" + + print("Running qmake in '{}' with Qt {}...".format(build_dir, qt_version)) + run(["qmake"] + configure_opts + [pro_files[0]], env=env_config, + **proc_opts) + + print("\nRunning make...") + run(make_args, env=env, **proc_opts) + + print("\nCleaning up...") + print("") + shutil.rmtree(build_dir) + elif(any([os.path.exists(os.path.join(project_dir, x)) for x in ["GNUmakefile", "makefile", "Makefile"]])): - # Make + # make # needs to be handled last, since other build systems can generate Makefiles print("Preparing build directory...") run([make_cmd, "clean"], env=env, **proc_opts) diff --git a/fake-toolchain/Unix/ar b/fake-toolchain/Unix/ar index 63b10de421..f32a5804e2 120000 --- a/fake-toolchain/Unix/ar +++ b/fake-toolchain/Unix/ar @@ -1 +1 @@ -/bin/true \ No newline at end of file +true \ No newline at end of file diff --git a/fake-toolchain/Unix/as b/fake-toolchain/Unix/as index 63b10de421..f32a5804e2 120000 --- a/fake-toolchain/Unix/as +++ b/fake-toolchain/Unix/as @@ -1 +1 @@ -/bin/true \ No newline at end of file +true \ No newline at end of file diff --git a/fake-toolchain/Unix/cc b/fake-toolchain/Unix/cc index 556f226f98..03e796105b 100755 --- a/fake-toolchain/Unix/cc +++ b/fake-toolchain/Unix/cc @@ -1,10 +1,10 @@ -#!/bin/bash +#!/bin/sh if [[ ! -z "$YCM_CONFIG_GEN_CC_PASSTHROUGH" ]]; then # Cmake determines compiler properties by compiling a test file, so call clang for this case $YCM_CONFIG_GEN_CC_PASSTHROUGH $@ -elif [[ "$@" == "-v" ]]; then +elif [ "$@" = "-v" ]; then # Needed to enable clang-specific options for certain build systems (e.g. linux) echo "clang version 3.5.0 (fake toolchain)" diff --git a/fake-toolchain/Unix/cxx b/fake-toolchain/Unix/cxx index 2b302dbdc5..3db8744404 100755 --- a/fake-toolchain/Unix/cxx +++ b/fake-toolchain/Unix/cxx @@ -1,10 +1,10 @@ -#!/bin/bash +#!/bin/sh if [[ ! -z "$YCM_CONFIG_GEN_CXX_PASSTHROUGH" ]]; then # Cmake determines compiler properties by compiling a test file, so call clang for this case $YCM_CONFIG_GEN_CXX_PASSTHROUGH $@ -elif [[ "$@" == "-v" ]]; then +elif [ "$@" = "-v" ]; then # Needed to enable clang-specific options for certain build systems (e.g. linux) echo "clang version 3.5.0 (fake toolchain)" diff --git a/fake-toolchain/Unix/g++ b/fake-toolchain/Unix/g++ new file mode 120000 index 0000000000..34836c6976 --- /dev/null +++ b/fake-toolchain/Unix/g++ @@ -0,0 +1 @@ +cxx \ No newline at end of file diff --git a/fake-toolchain/Unix/ld b/fake-toolchain/Unix/ld index 63b10de421..f32a5804e2 120000 --- a/fake-toolchain/Unix/ld +++ b/fake-toolchain/Unix/ld @@ -1 +1 @@ -/bin/true \ No newline at end of file +true \ No newline at end of file diff --git a/fake-toolchain/Unix/nm b/fake-toolchain/Unix/nm index 63b10de421..f32a5804e2 120000 --- a/fake-toolchain/Unix/nm +++ b/fake-toolchain/Unix/nm @@ -1 +1 @@ -/bin/true \ No newline at end of file +true \ No newline at end of file diff --git a/fake-toolchain/Unix/true b/fake-toolchain/Unix/true new file mode 100755 index 0000000000..420376f58a --- /dev/null +++ b/fake-toolchain/Unix/true @@ -0,0 +1,4 @@ +#!/bin/sh +# This script is needed because /bin/true does not exist on non-FHS-compliant distros. e.g. NixOS +exit 0 + diff --git a/plugin/ycm-generator.vim b/plugin/ycm-generator.vim index 9067a1f30e..f91b3ab66a 100644 --- a/plugin/ycm-generator.vim +++ b/plugin/ycm-generator.vim @@ -10,7 +10,7 @@ function! s:GenerateConfig(fmt, flags) " Only append the working directory if the last option is a flag let l:split_flags = split(a:flags) if len(l:split_flags) == 0 || l:split_flags[-1] =~ "^-" - let l:cmd = l:cmd . " " . getcwd() + let l:cmd = l:cmd . " " . fnameescape(getcwd()) endif execute l:cmd