From a7a46b2949cd0399e2747c2286ff0195aa0a805d Mon Sep 17 00:00:00 2001 From: ehuss Date: Sat, 19 Aug 2017 11:00:22 -0700 Subject: [PATCH] Add global Cargo settings for all packages. (#197) --- SyntaxCheckPlugin.py | 4 +- cargo_build.py | 26 +-- docs/build.md | 68 ++++++-- rust/cargo_config.py | 301 ++++++++++++++++++++--------------- rust/cargo_settings.py | 282 ++++++++++++++++++++++---------- rust/util.py | 8 +- tests/rust_test_common.py | 31 +++- tests/test_cargo_build.py | 39 ++--- tests/test_cargo_settings.py | 56 +++++++ 9 files changed, 555 insertions(+), 260 deletions(-) create mode 100644 tests/test_cargo_settings.py diff --git a/SyntaxCheckPlugin.py b/SyntaxCheckPlugin.py index 141f57d3..454c06ae 100755 --- a/SyntaxCheckPlugin.py +++ b/SyntaxCheckPlugin.py @@ -124,7 +124,7 @@ def get_rustc_messages(self): if method == 'clippy': # Clippy does not support cargo target filters, must be run for # all targets. - cmd = settings.get_command(command_info, self.cwd) + cmd = settings.get_command(method, command_info, self.cwd) p = rust_proc.RustProc() p.run(self.window, cmd['command'], self.cwd, self, env=cmd['env']) p.wait() @@ -134,7 +134,7 @@ def get_rustc_messages(self): td = target_detect.TargetDetector(self.window) targets = td.determine_targets(self.triggered_file_name) for (target_src, target_args) in targets: - cmd = settings.get_command(command_info, self.cwd, + cmd = settings.get_command(method, command_info, self.cwd, initial_settings={'target': ' '.join(target_args)}) if method == 'no-trans': cmd['command'].extend(['--', '-Zno-trans', '-Zunstable-options']) diff --git a/cargo_build.py b/cargo_build.py index 26234bb2..932bc37a 100644 --- a/cargo_build.py +++ b/cargo_build.py @@ -18,16 +18,16 @@ class CargoExecCommand(sublime_plugin.WindowCommand): This takes the following arguments: - - `command`: The command to run. Commands are defined in the + - `command`: The command name to run. Commands are defined in the `cargo_settings` module. You can define your own custom command by passing in `command_info`. - `command_info`: Dictionary of values the defines how the cargo command - is constructed. See `command_settings.CARGO_COMMANDS`. + is constructed. See `cargo_settings.CARGO_COMMANDS`. - `settings`: Dictionary of settings overriding anything set in the - Sublime project settings (see `command_settings` module). + Sublime project settings (see `cargo_settings` module). """ - # The combined command info from `command_settings` and whatever the user + # The combined command info from `cargo_settings` and whatever the user # passed in. command_info = None # Dictionary of initial settings passed in by the user. @@ -49,7 +49,7 @@ def run(self, command=None, command_info=None, settings=None): if command == 'auto': self._detect_auto_build() else: - self.command = command + self.command_name = command self.command_info = cargo_settings.CARGO_COMMANDS\ .get(command, {}).copy() if command_info: @@ -117,7 +117,7 @@ def _determine_working_path(self, on_done): self.settings_path = script_path return on_done() - default_path = self.settings.get('default_path') + default_path = self.settings.get_project_base('default_path') if default_path: self.settings_path = default_path if os.path.isfile(default_path): @@ -153,7 +153,7 @@ def _run_check_for_args(self): if self.command_info.get('wants_run_args', False) and \ not self.initial_settings.get('extra_run_args'): self.window.show_input_panel('Enter extra args:', - LAST_EXTRA_ARGS.get(self.command, ''), + LAST_EXTRA_ARGS.get(self.command_name, ''), self._on_extra_args, None, None) else: self._run() @@ -165,7 +165,8 @@ def _on_extra_args(self, args): def _run(self): t = CargoExecThread(self.window, self.settings, - self.command_info, self.initial_settings, + self.command_name, self.command_info, + self.initial_settings, self.settings_path, self.working_dir) t.start() @@ -175,17 +176,20 @@ class CargoExecThread(rust_thread.RustThread): silently_interruptible = False name = 'Cargo Exec' - def __init__(self, window, settings, command_info, initial_settings, - settings_path, working_dir): + def __init__(self, window, settings, + command_name, command_info, + initial_settings, settings_path, working_dir): super(CargoExecThread, self).__init__(window) self.settings = settings + self.command_name = command_name self.command_info = command_info self.initial_settings = initial_settings self.settings_path = settings_path self.working_dir = working_dir def run(self): - cmd = self.settings.get_command(self.command_info, + cmd = self.settings.get_command(self.command_name, + self.command_info, self.settings_path, self.initial_settings) if not cmd: diff --git a/docs/build.md b/docs/build.md index 976bea56..6f82966b 100644 --- a/docs/build.md +++ b/docs/build.md @@ -61,19 +61,21 @@ It also supports Sublime's build settings: | `show_errors_inline` | `true` | If true, messages are displayed in line using Sublime's phantoms. If false, messages are only displayed in the output panel. | | `show_panel_on_build` | `true` | If true, an output panel is displayed at the bottom of the window showing the compiler output. | -## Cargo Project Settings +## Cargo Settings -You can customize how Cargo is run with settings stored in your -`sublime-project` file. Settings can be applied per-target (`--lib`, -`--example foo`, etc.), for specific variants ("Build", "Run", "Test", etc.), -or globally. +A variety of settings are available to customize how Cargo is run. These +settings can be set globally, per-Sublime project, per-Cargo package, for +specific build variants ("Build", "Run", "Test", etc.), or specific Cargo +targets (`--lib`, `--example foo`, etc.). ### Configure Command To help you configure the Cargo build settings, run the `Rust: Configure Cargo -Build` command from Sublime's Command Palette (Ctrl-Shift-P / ⌘-Shift-P). -This will ask you a series of questions for the setting to configure. The -first choice is the setting: +Build` command from Sublime's Command Palette (Ctrl-Shift-P / ⌘-Shift-P). This +will ask you a series of questions for the setting to configure. It will +update your `.sublime-project` or `Users/RustEnhanced.sublime-settings` file +depending on which options you pick. The first question is the setting you +want to update: Setting | Description ------- | ----------- @@ -88,21 +90,30 @@ Default Package/Path | The default package to build, useful if you have a worksp If you have multiple Cargo packages in your workspace, it will ask for the package to configure. -A setting can be global (for all build invocations), for a specific build variant (such as "Test"), or for a specific build target (such as `--example myprog`). - Caution: If you have not created a `sublime-project` file, then any changes you make will be lost if you close the Sublime window. ### Settings -Settings are stored in your `sublime-project` file under `"cargo_build"` in -the `"settings"` key. Settings are organized per Cargo package in the -`"paths"` object. Paths can either be directories to a Cargo package, or the -path to a Rust source file (when used with `cargo script`). The top-level -keys for each package are: +Cargo settings are stored in the `"cargo_build"` Sublime setting. This can be +either in your `sublime-project` file or in +`Users/RustEnhanced.sublime-settings`. `"cargo_build"` is an object with the +following keys: Key | Description --- | ----------- +`"paths"` | Settings for specific Cargo packages. +`"default_path"` | The default Cargo package to build (useful for workspaces, see below). +`"variants"` | Settings per build variant. +`"defaults"` | Default settings used if not set per target or variant. + +Paths should be an absolute path to the directory of a Cargo package, or the +path to a Rust source file (when used with `cargo script`). + +`"paths"` is an object of path keys mapping to an object with the keys: + +Path Key | Description +-------- | ----------- `"defaults"` | Default settings used if not set per target or variant. `"targets"` | Settings per target (such as `"--lib"` or `"--bin foo"`). `"variants"` | Settings per build variant. @@ -119,7 +130,7 @@ An example of a `sublime-project` file: "paths": { "/path/to/package": { "defaults": { - "release": true + "release": false }, "targets": { "--example ex1": { @@ -136,7 +147,17 @@ An example of a `sublime-project` file: } } }, - "default_path": "/path/to/package" + "default_path": "/path/to/package", + "variants": { + "run": { + "env": { + "RUST_BACKTRACE": 1 + } + } + }, + "defaults": { + "release": true + } } } } @@ -162,6 +183,19 @@ The extra args settings support standard Sublime variable expansion (see [Build System Variables](http://docs.sublimetext.info/en/latest/reference/build_systems/configuration.html#build-system-variables)) +### Setting Precedence + +The Cargo commands will generally use the most specific setting available. +The order they are searched are (first found value wins): + +1. `.sublime-project` > Cargo Package > Cargo Target +2. `.sublime-project` > Cargo Package > Build Variant +3. `.sublime-project` > Cargo Package > Defaults +4. `.sublime-project` > Build Variant +5. `RustEnhanced.sublime-settings` > Build Variant +6. `.sublime-project` > Defaults +7. `RustEnhanced.sublime-settings` > Defaults + ## Multiple Cargo Projects (Advanced) You can have multiple Cargo projects in a single Sublime project (such as when diff --git a/rust/cargo_config.py b/rust/cargo_config.py index a24075e8..5e2b61e7 100644 --- a/rust/cargo_config.py +++ b/rust/cargo_config.py @@ -73,6 +73,9 @@ class CargoConfigBase(sublime_plugin.WindowCommand): # from the active view if it is available. package_allows_active_view_shortcut = True + # If True, 'which' will only allow you choose a package-specific setting. + which_requires_package = False + # This is a dictionary populated by the `items_package` method. # Key is the path to a package, the value is the metadata from Cargo. # This is used by other questions (like `items_target`) to get more @@ -105,13 +108,6 @@ def show_next_question(self): self.done() return - try: - item_info = getattr(self, 'items_' + q)() - except CancelCommandError: - return - if not isinstance(item_info, dict): - item_info = {'items': item_info} - f_selected = getattr(self, 'selected_' + q, None) # Called with the result of what the user selected. @@ -130,6 +126,13 @@ def make_choice(value): if q in self.input: make_choice(self.input[q]) else: + try: + item_info = getattr(self, 'items_' + q)() + except CancelCommandError: + return + if not isinstance(item_info, dict): + item_info = {'items': item_info} + if 'items' in item_info: def wrapper(index): if index != -1: @@ -231,6 +234,7 @@ def display_name(package): } def items_target(self): + """Choosing a target requires that 'package' has already been chosen.""" # Group by kind. kinds = {} package_path = self.choices['package'] @@ -249,7 +253,7 @@ def items_target(self): pass else: print('Rust: Unsupported target found: %s' % kind) - items = [('All Targets', None)] + items = [] for kind, values in kinds.items(): allowed = True if self.choices.get('variant', None): @@ -259,6 +263,8 @@ def items_target(self): allowed = kind in target_types if allowed: items.extend(values) + if not items: + sublime.error_message('Could not determine available targets.') return items def items_variant(self): @@ -277,61 +283,150 @@ def filter_variant(self, x): return x['name'] != 'no-trans' def items_which(self): + """Choice to select at which level the setting should be saved at.""" # This is a bit of a hack so that when called programmatically you # don't have to specify 'which'. if 'which' not in self.input: if 'variant' in self.input: - self.input['which'] = 'variant' + self.input['which'] = 'project_package_variant' elif 'target' in self.input: - self.input['which'] = 'target' + self.input['which'] = 'project_package_target' - return [ - (['Configure %s for all build commands.' % self.config_name, - ''], 'default'), - (['Configure %s for a Build Variant.' % self.config_name, - 'cargo build, cargo run, cargo test, etc.'], 'variant'), - (['Configure %s for a Target.' % self.config_name, - '--bin, --example, --test, etc.'], 'target') - ] + variant_extra = 'cargo build, cargo run, cargo test, etc.' + target_extra = '--bin, --example, --test, etc.' + result = [] + if not self.which_requires_package: + result.extend([ + (['Set %s globally.', 'Updates RustEnhanced.sublime-settings'], + 'global_default'), + (['Set %s in this Sublime project.', ''], + 'project_default'), + (['Set %s globally for a Build Variant.', variant_extra], + 'global_variant'), + (['Set %s in this Sublime project for a Build Variant (all Cargo packages).', variant_extra], + 'project_variant'), + ]) + result.extend([ + (['Set %s in this Sublime project for all commands (specific Cargo package).', ''], + 'project_package_default'), + (['Set %s in this Sublime project for a Build Variant (specific Cargo package).', variant_extra], + 'project_package_variant'), + (['Set %s in this Sublime project for a Target (specific Cargo package).', target_extra], + 'project_package_target'), + ]) + for (text, _) in result: + text[0] = text[0] % (self.config_name,) + return result def selected_which(self, which): - if which == 'default': - self.choices['target'] = None - return - elif which == 'variant': + if which in ('project_variant', 'global_variant'): return ['variant'] - elif which == 'target': - return ['target'] - else: - raise AssertionError(which) + elif which == 'project_package_default': + return ['package'] + elif which == 'project_package_variant': + return ['package', 'variant'] + elif which == 'project_package_target': + return ['package', 'target'] def get_setting(self, name, default=None): """Retrieve a setting, honoring the "which" selection.""" - if self.choices['which'] == 'variant': - return self.settings.get_with_variant(self.choices['package'], - self.choices['variant'], - name, default=default) - elif self.choices['which'] in ('default', 'target'): - return self.settings.get_with_target(self.choices['package'], - self.choices['target'], - name, default=default) + w = self.choices['which'] + if w == 'global_default': + return self.settings.get_global_default(name, default) + elif w == 'project_default': + return self.settings.get_project_default(name, default) + elif w == 'global_variant': + return self.settings.get_global_variant(self.choices['variant'], + name, default) + elif w == 'project_variant': + return self.settings.get_project_variant(self.choices['variant'], + name, default) + elif w == 'project_package_default': + return self.settings.get_project_package_default( + self.choices['package'], name, default) + elif w == 'project_package_variant': + return self.settings.get_project_package_variant( + self.choices['package'], self.choices['variant'], name, default) + elif w == 'project_package_target': + return self.settings.get_project_package_target( + self.choices['package'], self.choices['target'], name, default) else: - raise AssertionError(self.choices['which']) + raise AssertionError(w) def set_setting(self, name, value): """Set a setting, honoring the "which" selection.""" - if self.choices['which'] == 'variant': - self.settings.set_with_variant(self.choices['package'], - self.choices['variant'], - name, - value) - elif self.choices['which'] in ('default', 'target'): - self.settings.set_with_target(self.choices['package'], - self.choices['target'], - name, - value) + w = self.choices['which'] + if w == 'global_default': + return self.settings.set_global_default(name, value) + elif w == 'project_default': + return self.settings.set_project_default(name, value) + elif w == 'global_variant': + return self.settings.set_global_variant(self.choices['variant'], + name, value) + elif w == 'project_variant': + return self.settings.set_project_variant(self.choices['variant'], + name, value) + elif w == 'project_package_default': + return self.settings.set_project_package_default( + self.choices['package'], name, value) + elif w == 'project_package_variant': + return self.settings.set_project_package_variant( + self.choices['package'], self.choices['variant'], name, value) + elif w == 'project_package_target': + return self.settings.set_project_package_target( + self.choices['package'], self.choices['target'], name, value) else: - raise AssertionError(self.choices['which']) + raise AssertionError(w) + + toolchain_allows_default = True + + def items_toolchain(self): + items = [] + if self.toolchain_allows_default: + items.append(('Use Default Toolchain', None)) + toolchains = self._toolchain_list() + current = self.get_setting('toolchain') + items.extend([(x, x) for x in toolchains]) + result = { + 'items': items, + } + if self.toolchain_allows_default or current: + result['default'] = current + return result + + def _toolchain_list(self): + output = rust_proc.check_output(self.window, + 'rustup toolchain list'.split(), + None) + output = output.splitlines() + system_default = index_with(output, lambda x: x.endswith(' (default)')) + if system_default != -1: + # Strip the " (default)" text. + output[system_default] = output[system_default][:-10] + # Rustup supports some shorthand of either `channel` or `channel-date` + # without the trailing target info. + # + # Complete list of available toolchains is available at: + # https://static.rust-lang.org/dist/index.html + # (See https://github.com/rust-lang-nursery/rustup.rs/issues/215) + shorthands = [] + channels = ['nightly', 'beta', 'stable', '\d\.\d{1,2}\.\d'] + pattern = '(%s)(?:-(\d{4}-\d{2}-\d{2}))?(?:-(.*))' % '|'.join(channels) + for toolchain in output: + m = re.match(pattern, toolchain) + # Should always match. + if m: + channel = m.group(1) + date = m.group(2) + if date: + shorthand = '%s-%s' % (channel, date) + else: + shorthand = channel + if shorthand not in shorthands: + shorthands.append(shorthand) + result = shorthands + output + result.sort() + return result class CargoConfigPackage(CargoConfigBase): @@ -354,7 +449,7 @@ def done(self): class CargoSetProfile(CargoConfigBase): config_name = 'Profile' - sequence = ['package', 'which', 'profile'] + sequence = ['which', 'profile'] def items_profile(self): default = self.get_setting('release', False) @@ -382,33 +477,37 @@ def filter_variant(self, info): def items_target(self): items = super(CargoSetTarget, self).items_target() - items.insert(1, ('Automatic Detection', 'auto')) - default = self.settings.get_with_variant(self.choices['package'], - self.choices['variant'], - 'target') - return { - 'items': items, - 'default': default + items.insert(0, ('Automatic Detection', 'auto')) + default = self.settings.get_project_package_variant( + self.choices['package'], self.choices['variant'], 'target') + result = { + 'items': items } + if default: + result['default'] = default + return result def done(self): - self.settings.set_with_variant(self.choices['package'], - self.choices['variant'], - 'target', - self.choices['target']) + self.settings.set_project_package_variant(self.choices['package'], + self.choices['variant'], + 'target', + self.choices['target']) class CargoSetTriple(CargoConfigBase): config_name = 'Triple' - sequence = ['package', 'which', 'target_triple'] + sequence = ['which', 'toolchain', 'target_triple'] + toolchain_allows_default = False def items_target_triple(self): # Could check if rustup is not installed, to run # "rustc --print target-list", but that does not tell # us which targets are installed. - triples = rust_proc.check_output(self.window, - 'rustup target list'.split(), self.choices['package'])\ + + # The target list depends on the toolchain used. + cmd = 'rustup target list --toolchain=%s' % self.choices['toolchain'] + triples = rust_proc.check_output(self.window, cmd.split(), None)\ .splitlines() current = self.get_setting('target_triple') result = [('Use Default', None)] @@ -434,50 +533,7 @@ def done(self): class CargoSetToolchain(CargoConfigBase): config_name = 'Toolchain' - sequence = ['package', 'which', 'toolchain'] - - def items_toolchain(self): - items = [('Use Default Toolchain', None)] - toolchains = self._toolchain_list() - current = self.get_setting('toolchain') - items.extend([(x, x) for x in toolchains]) - return { - 'items': items, - 'default': current - } - - def _toolchain_list(self): - output = rust_proc.check_output(self.window, - 'rustup toolchain list'.split(), - self.choices['package']) - output = output.splitlines() - system_default = index_with(output, lambda x: x.endswith(' (default)')) - if system_default != -1: - output[system_default] = output[system_default][:-10] - # Rustup supports some shorthand of either `channel` or `channel-date` - # without the trailing target info. - # - # Complete list of available toolchains is available at: - # https://static.rust-lang.org/dist/index.html - # (See https://github.com/rust-lang-nursery/rustup.rs/issues/215) - shorthands = [] - channels = ['nightly', 'beta', 'stable', '\d\.\d{1,2}\.\d'] - pattern = '(%s)(?:-(\d{4}-\d{2}-\d{2}))?(?:-(.*))' % '|'.join(channels) - for toolchain in output: - m = re.match(pattern, toolchain) - # Should always match. - if m: - channel = m.group(1) - date = m.group(2) - if date: - shorthand = '%s-%s' % (channel, date) - else: - shorthand = channel - if shorthand not in shorthands: - shorthands.append(shorthand) - result = shorthands + output - result.sort() - return result + sequence = ['which', 'toolchain'] def done(self): self.set_setting('toolchain', self.choices['toolchain']) @@ -486,7 +542,8 @@ def done(self): class CargoSetFeatures(CargoConfigBase): config_name = 'Features' - sequence = ['package', 'which', 'no_default_features', 'features'] + sequence = ['which', 'no_default_features', 'features'] + which_requires_package = True def items_no_default_features(self): current = self.get_setting('no_default_features', False) @@ -538,17 +595,17 @@ def items_package(self): items.insert(0, (['No Default', 'Build will attempt to detect from the current view, or pop up a selection panel.'], None)) - result['default'] = self.settings.get('default_path') + result['default'] = self.settings.get_project_base('default_path') return result def done(self): - self.settings.set('default_path', self.choices['package']) + self.settings.set_project_base('default_path', self.choices['package']) class CargoSetEnvironmentEditor(CargoConfigBase): config_name = 'Environment' - sequence = ['package', 'which'] + sequence = ['which'] def done(self): view = self.window.new_file() @@ -576,7 +633,7 @@ def done(self): view.set_syntax_file('Packages/JavaScript/JSON.sublime-syntax') view.settings().set('rust_environment_editor', True) view.settings().set('rust_environment_editor_settings', { - 'package': self.choices['package'], + 'package': self.choices.get('package'), 'which': self.choices['which'], 'variant': self.choices.get('variant'), 'target': self.choices.get('target'), @@ -589,7 +646,7 @@ class CargoSetEnvironment(CargoConfigBase): on-close callback to actually set the environment.""" config_name = 'Environment' - sequence = ['package', 'which', 'env'] + sequence = ['which', 'env'] def items_env(self): return [] @@ -613,19 +670,19 @@ def on_pre_close(self, view): except: sublime.error_message('Value was not valid JSON, try again.') view.window().run_command('cargo_set_environment_editor', { - 'package': settings['package'], + 'package': settings.get('package'), 'which': settings['which'], - 'variant': settings['variant'], - 'target': settings['target'], + 'variant': settings.get('variant'), + 'target': settings.get('target'), 'contents': contents, }) return view.window().run_command('cargo_set_environment', { - 'package': settings['package'], + 'package': settings.get('package'), 'which': settings['which'], - 'variant': settings['variant'], - 'target': settings['target'], + 'variant': settings.get('variant'), + 'target': settings.get('target'), 'env': result, }) @@ -633,7 +690,7 @@ def on_pre_close(self, view): class CargoSetArguments(CargoConfigBase): config_name = 'Extra Command-line Arguments' - sequence = ['package', 'which', 'before_after', 'args'] + sequence = ['which', 'before_after', 'args'] def items_before_after(self): return [ @@ -689,14 +746,6 @@ def selected_config_option(self, which): else: raise AssertionError(which) - def selected_which(self, which): - if which == 'variant': - return ['package', 'variant', 'toolchain'] - elif which == 'target': - return ['package', 'target', 'toolchain'] - else: - raise AssertionError(which) - def done(self): pass diff --git a/rust/cargo_settings.py b/rust/cargo_settings.py index 680394e2..474af524 100644 --- a/rust/cargo_settings.py +++ b/rust/cargo_settings.py @@ -143,61 +143,124 @@ def load(self): if self.project_data is None: # Window does not have a Sublime project. self.project_data = {} + self.re_settings = sublime.load_settings('RustEnhanced.sublime-settings') - def get(self, key, default=None): + def get_global_default(self, key, default=None): + return self.re_settings.get('cargo_build', {})\ + .get('defaults', {})\ + .get(key, default) + + def set_global_default(self, key, value): + cb = self.re_settings.get('cargo_build', {}) + cb.setdefault('defaults', {})[key] = value + self.re_settings.set('cargo_build', cb) + sublime.save_settings('RustEnhanced.sublime-settings') + + def get_project_default(self, key, default=None): return self.project_data.get('settings', {})\ .get('cargo_build', {})\ + .get('defaults', {})\ .get(key, default) - def set(self, key, value): + def set_project_default(self, key, value): self.project_data.setdefault('settings', {})\ - .setdefault('cargo_build', {})[key] = value + .setdefault('cargo_build', {})\ + .setdefault('defaults', {})[key] = value + self._set_project_data() + + def get_global_variant(self, variant, key, default=None): + return self.re_settings.get('cargo_build', {})\ + .get('variants', {})\ + .get(variant, {})\ + .get(key, default) + + def set_global_variant(self, variant, key, value): + cb = self.re_settings.get('cargo_build', {}) + cb.setdefault('variants', {})\ + .setdefault(variant, {})[key] = value + self.re_settings.set('cargo_build', cb) + sublime.save_settings('RustEnhanced.sublime-settings') + + def get_project_variant(self, variant, key, default=None): + return self.project_data.get('settings', {})\ + .get('cargo_build', {})\ + .get('variants', {})\ + .get(variant, {})\ + .get(key, default) + + def set_project_variant(self, variant, key, value): + self.project_data.setdefault('settings', {})\ + .setdefault('cargo_build', {})\ + .setdefault('variants', {})\ + .setdefault(variant, {})[key] = value self._set_project_data() - def get_with_target(self, path, target, key, default=None): + def get_project_package_default(self, path, key, default=None): path = os.path.normpath(path) - pdata = self.project_data.get('settings', {})\ - .get('cargo_build', {})\ - .get('paths', {})\ - .get(path, {}) - if target: - d = pdata.get('targets', {}).get(target, {}) - else: - d = pdata.get('defaults', {}) - return d.get(key, default) + return self.project_data.get('settings', {})\ + .get('cargo_build', {})\ + .get('paths', {})\ + .get(path, {})\ + .get('defaults', {})\ + .get(key, default) + + def set_project_package_default(self, path, key, value): + path = os.path.normpath(path) + self.project_data.setdefault('settings', {})\ + .setdefault('cargo_build', {})\ + .setdefault('paths', {})\ + .setdefault(path, {})\ + .setdefault('defaults', {})[key] = value + self._set_project_data() - def get_with_variant(self, path, variant, key, default=None): + def get_project_package_variant(self, path, variant, key, default=None): path = os.path.normpath(path) - vdata = self.project_data.get('settings', {})\ - .get('cargo_build', {})\ - .get('paths', {})\ - .get(path, {})\ - .get('variants', {})\ - .get(variant, {}) - return vdata.get(key, default) - - def set_with_target(self, path, target, key, value): + return self.project_data.get('settings', {})\ + .get('cargo_build', {})\ + .get('paths', {})\ + .get(path, {})\ + .get('variants', {})\ + .get(variant, {})\ + .get(key, default) + + def set_project_package_variant(self, path, variant, key, value): path = os.path.normpath(path) - pdata = self.project_data.setdefault('settings', {})\ - .setdefault('cargo_build', {})\ - .setdefault('paths', {})\ - .setdefault(path, {}) - if target: - d = pdata.setdefault('targets', {}).setdefault(target, {}) - else: - d = pdata.setdefault('defaults', {}) - d[key] = value + self.project_data.setdefault('settings', {})\ + .setdefault('cargo_build', {})\ + .setdefault('paths', {})\ + .setdefault(path, {})\ + .setdefault('variants', {})\ + .setdefault(variant, {})[key] = value self._set_project_data() - def set_with_variant(self, path, variant, key, value): + def get_project_package_target(self, path, target, key, default=None): + path = os.path.normpath(path) + return self.project_data.get('settings', {})\ + .get('cargo_build', {})\ + .get('paths', {})\ + .get(path, {})\ + .get('targets', {})\ + .get(target, {})\ + .get(key, default) + + def set_project_package_target(self, path, target, key, value): path = os.path.normpath(path) - vdata = self.project_data.setdefault('settings', {})\ - .setdefault('cargo_build', {})\ - .setdefault('paths', {})\ - .setdefault(path, {})\ - .setdefault('variants', {})\ - .setdefault(variant, {}) - vdata[key] = value + self.project_data.setdefault('settings', {})\ + .setdefault('cargo_build', {})\ + .setdefault('paths', {})\ + .setdefault(path, {})\ + .setdefault('targets', {})\ + .setdefault(target, {})[key] = value + self._set_project_data() + + def get_project_base(self, key, default=None): + return self.project_data.get('settings', {})\ + .get('cargo_build', {})\ + .get(key, default) + + def set_project_base(self, key, value): + self.project_data.setdefault('settings', {})\ + .setdefault('cargo_build', {})[key] = value self._set_project_data() def _set_project_data(self): @@ -209,31 +272,17 @@ def _set_project_data(self): Any changes to the Cargo build settings will be lost if you close the window.""")) self.window.set_project_data(self.project_data) - def get_command(self, cmd_info, settings_path, initial_settings={}): - """Generates the command arguments for running Cargo. - - :Returns: A dictionary with the keys: - - `command`: The command to run as a list of strings. - - `env`: Dictionary of environment variables (or None). - - Returns None if the command cannot be constructed. - """ - command = cmd_info['command'] - result = ['cargo'] - pdata = self.project_data.get('settings', {})\ - .get('cargo_build', {})\ - .get('paths', {})\ - .get(settings_path, {}) - vdata = pdata.get('variants', {})\ - .get(command, {}) - - def vdata_get(key, default=None): - return initial_settings.get(key, vdata.get(key, default)) + def determine_target(self, cmd_name, settings_path, + cmd_info=None, override=None): + if cmd_info is None: + cmd_info = CARGO_COMMANDS[cmd_name] - # Target target = None if cmd_info.get('allows_target', False): - tcfg = vdata_get('target') + if override: + tcfg = override + else: + tcfg = self.get_project_package_variant(settings_path, cmd_name, 'target') if tcfg == 'auto': # If this fails, leave target as None and let Cargo sort it # out (it may display an error). @@ -246,14 +295,89 @@ def vdata_get(key, default=None): target = ' '.join(cmd_line) else: target = tcfg + return target + + def get_computed(self, settings_path, variant, target, key, + default=None, initial_settings={}): + """Get the configuration value for the given key.""" + v = initial_settings.get(key) + if v is None: + v = self.get_project_package_target(settings_path, target, key) + if v is None: + v = self.get_project_package_variant(settings_path, variant, key) + if v is None: + v = self.get_project_package_default(settings_path, key) + if v is None: + v = self.get_project_variant(variant, key) + if v is None: + v = self.get_global_variant(variant, key) + if v is None: + v = self.get_project_default(key) + if v is None: + v = self.get_global_default(key, default) + return v + + def get_merged(self, settings_path, variant, target, key, + initial_settings={}): + """Get the configuration value for the given key. + + This assumes the value is a dictionary, and will merge all values from + each level. This is primarily used for the `env` environment + variables. + """ + result = self.get_global_default(key, {}).copy() + + proj_def = self.get_project_default(key, {}) + result.update(proj_def) + + glbl_var = self.get_global_variant(variant, key, {}) + result.update(glbl_var) - def get(key, default=None): - d = pdata.get('defaults', {}).get(key, default) - v_val = vdata.get(key, d) - t_val = pdata.get('targets', {}).get(target, {}).get(key, v_val) - return initial_settings.get(key, t_val) + proj_var = self.get_project_variant(variant, key, {}) + result.update(proj_var) + + pp_def = self.get_project_package_default(settings_path, key, {}) + result.update(pp_def) + + pp_var = self.get_project_package_variant(settings_path, variant, key, {}) + result.update(pp_var) + + pp_tar = self.get_project_package_target(settings_path, target, key, {}) + result.update(pp_tar) + + initial = initial_settings.get(key, {}) + result.update(initial) + return result + + def get_command(self, cmd_name, cmd_info, + settings_path, initial_settings={}): + """Generates the command arguments for running Cargo. + + :param cmd_name: The name of the command, the key used to select a + "variant". + :param cmd_info: Dictionary from `CARGO_COMMANDS` with rules on how to + construct the command. + :param settings_path: The absolute path to the Cargo project root + directory. + :keyword initial_settings: Initial settings to inject which override + all other settings. + + :Returns: A dictionary with the keys: + - `command`: The command to run as a list of strings. + - `env`: Dictionary of environment variables (or None). + + Returns None if the command cannot be constructed. + """ + target = self.determine_target(cmd_name, settings_path, + cmd_info=cmd_info, override=initial_settings.get('target')) + + def get_computed(key, default=None): + return self.get_computed(settings_path, cmd_name, target, key, + default=default, initial_settings=initial_settings) + + result = ['cargo'] - toolchain = get('toolchain', None) + toolchain = get_computed('toolchain', None) if toolchain: result.append('+' + toolchain) @@ -266,13 +390,13 @@ def get(key, default=None): # target_triple if cmd_info.get('allows_target_triple', False): - v = get('target_triple', None) + v = get_computed('target_triple', None) if v: result.extend(['--target', v]) # release (profile) if cmd_info.get('allows_release', False): - v = get('release', False) + v = get_computed('release', False) if v: result.append('--release') @@ -282,10 +406,10 @@ def get(key, default=None): # features if cmd_info.get('allows_features', False): - v = get('no_default_features', False) + v = get_computed('no_default_features', False) if v: result.append('--no-default-features') - v = get('features', None) + v = get_computed('features', None) if v: if v.upper() == 'ALL': result.append('--all-features') @@ -295,11 +419,11 @@ def get(key, default=None): # Add path from current active view (mainly for "cargo script"). if cmd_info.get('requires_view_path', False): - script_path = get('script_path') + script_path = get_computed('script_path') if not script_path: if not util.active_view_is_rust(): sublime.error_message(util.multiline_fix(""" - Cargo build command %r requires the current view to be a Rust source file.""" % command)) + Cargo build command %r requires the current view to be a Rust source file.""" % cmd_info['name'])) return None script_path = self.window.active_view().file_name() result.append(script_path) @@ -309,22 +433,20 @@ def expand(s): self.window.extract_variables()) # Extra args. - extra_cargo_args = get('extra_cargo_args') + extra_cargo_args = get_computed('extra_cargo_args') if extra_cargo_args: extra_cargo_args = expand(extra_cargo_args) result.extend(shlex.split(extra_cargo_args)) - extra_run_args = get('extra_run_args') + extra_run_args = get_computed('extra_run_args') if extra_run_args: extra_run_args = expand(extra_run_args) result.append('--') result.extend(shlex.split(extra_run_args)) # Compute the environment. - env = pdata.get('defaults', {}).get('env', {}) - env.update(vdata.get('env', {})) - env.update(pdata.get('targets', {}).get(target, {}).get('env', {})) - env.update(initial_settings.get('env', {})) + env = self.get_merged(settings_path, cmd_name, target, 'env', + initial_settings=initial_settings) for k, v in env.items(): env[k] = os.path.expandvars(v) if not env: diff --git a/rust/util.py b/rust/util.py index c252cfa5..4692b210 100644 --- a/rust/util.py +++ b/rust/util.py @@ -56,13 +56,17 @@ def debug(msg, *args): print(msg % args) -def get_rustc_version(window, cwd): +def get_rustc_version(window, cwd, toolchain=None): """Returns the rust version for the given directory. :Returns: A string such as '1.16.0' or '1.17.0-nightly'. """ from . import rust_proc - output = rust_proc.check_output(window, ['rustc', '--version'], cwd) + cmd = ['rustc'] + if toolchain: + cmd.append('+' + toolchain) + cmd.append('--version') + output = rust_proc.check_output(window, cmd, cwd) # Example outputs: # rustc 1.15.1 (021bd294c 2017-02-08) # rustc 1.16.0-beta.2 (bc15d5281 2017-02-16) diff --git a/tests/rust_test_common.py b/tests/rust_test_common.py index d7785b3a..db4feb10 100644 --- a/tests/rust_test_common.py +++ b/tests/rust_test_common.py @@ -33,6 +33,11 @@ def unescape(s): .replace('>', '>') +# This is used to mark overridden configuration variables that should be +# deleted. +DELETE_SENTINEL = 'DELETE_SENTINEL' + + class TestBase(unittest.TestCase): def setUp(self): @@ -42,13 +47,31 @@ def setUp(self): if 'cargo_build' in data.get('settings', {}): del data['settings']['cargo_build'] window.set_project_data(data) - self.settings = sublime.load_settings('RustEnhanced.sublime-settings') - self._orig_show_panel = self.settings.get('show_panel_on_build') - self.settings.set('show_panel_on_build', False) plugin.cargo_build.ON_LOAD_MESSAGES_ENABLED = False + # Override settings. + self._original_settings = {} + self.settings = sublime.load_settings('RustEnhanced.sublime-settings') + self._override_setting('show_panel_on_build', False) + self._override_setting('cargo_build', {}) + + def _override_setting(self, name, value): + if name not in self._original_settings: + if self.settings.has(name): + self._original_settings[name] = self.settings.get(name) + else: + self._original_settings[name] = DELETE_SENTINEL + self.settings.set(name, value) + + def _restore_settings(self): + for key, value in self._original_settings.items(): + if value is DELETE_SENTINEL: + self.settings.erase(key) + else: + self.settings.set(key, value) + def tearDown(self): - self.settings.set('show_panel_on_build', self._orig_show_panel) + self._restore_settings() plugin.cargo_build.ON_LOAD_MESSAGES_ENABLED = True def _get_rust_thread(self): diff --git a/tests/test_cargo_build.py b/tests/test_cargo_build.py index e8ce9073..d6fd5c83 100644 --- a/tests/test_cargo_build.py +++ b/tests/test_cargo_build.py @@ -95,7 +95,7 @@ def test_profile(self): def _test_profile(self, view): window = view.window() - window.run_command('cargo_set_profile', {'target': None, + window.run_command('cargo_set_profile', {'which': 'project_default', 'profile': 'release'}) self._run_build_wait() self.assertTrue(os.path.exists( @@ -104,7 +104,7 @@ def _test_profile(self, view): os.path.join(multi_target_root, 'target/debug'))) self._cargo_clean(multi_target_root) - window.run_command('cargo_set_profile', {'target': None, + window.run_command('cargo_set_profile', {'which': 'project_default', 'profile': 'dev'}) self._run_build_wait() self.assertFalse(os.path.exists( @@ -121,13 +121,14 @@ def _test_target_triple(self, view): window = view.window() # Use a fake triple, since we don't want to assume what you have # installed. - window.run_command('cargo_set_triple', {'target': None, + window.run_command('cargo_set_triple', {'which': 'project_default', + 'toolchain': None, 'target_triple': 'a-b-c'}) settings = cargo_settings.CargoSettings(window) settings.load() cmd_info = cargo_settings.CARGO_COMMANDS['build'] manifest_dir = util.find_cargo_manifest(view.file_name()) - cmd = settings.get_command(cmd_info, manifest_dir)['command'] + cmd = settings.get_command('build', cmd_info, manifest_dir)['command'] self.assertEqual(cmd, ['cargo', 'build', '--target', 'a-b-c', '--message-format=json']) @@ -139,37 +140,37 @@ def test_toolchain(self): def _test_toolchain(self, view): window = view.window() # Variant - window.run_command('cargo_set_toolchain', {'which': 'variant', + window.run_command('cargo_set_toolchain', {'which': 'project_variant', 'variant': 'build', 'toolchain': 'nightly'}) settings = cargo_settings.CargoSettings(window) settings.load() cmd_info = cargo_settings.CARGO_COMMANDS['build'] manifest_dir = util.find_cargo_manifest(view.file_name()) - cmd = settings.get_command(cmd_info, manifest_dir)['command'] + cmd = settings.get_command('build', cmd_info, manifest_dir)['command'] self.assertEqual(cmd, ['cargo', '+nightly', 'build', '--message-format=json']) # Variant clear. - window.run_command('cargo_set_toolchain', {'which': 'variant', + window.run_command('cargo_set_toolchain', {'which': 'project_variant', 'variant': 'build', 'toolchain': None}) settings.load() cmd_info = cargo_settings.CARGO_COMMANDS['build'] manifest_dir = util.find_cargo_manifest(view.file_name()) - cmd = settings.get_command(cmd_info, manifest_dir)['command'] + cmd = settings.get_command('build', cmd_info, manifest_dir)['command'] self.assertEqual(cmd, ['cargo', 'build', '--message-format=json']) # Target - window.run_command('cargo_set_toolchain', {'which': 'target', + window.run_command('cargo_set_toolchain', {'which': 'project_package_target', 'target': '--bin bin1', 'toolchain': 'nightly'}) window.run_command('cargo_set_target', {'variant': 'build', 'target': '--bin bin1'}) settings.load() manifest_dir = util.find_cargo_manifest(view.file_name()) - cmd = settings.get_command(cmd_info, manifest_dir)['command'] + cmd = settings.get_command('build', cmd_info, manifest_dir)['command'] self.assertEqual(cmd, ['cargo', '+nightly', 'build', '--bin', 'bin1', '--message-format=json']) @@ -237,6 +238,7 @@ def test_check(self): self._test_check) def _test_check(self, view): + self._cargo_clean(view) self._run_build_wait('check', settings={'target': '--example err_ex'}) self._check_added_message(view.window(), view.file_name(), @@ -249,7 +251,7 @@ def test_bench(self): def _test_bench(self, view): window = view.window() - window.run_command('cargo_set_toolchain', {'which': 'variant', + window.run_command('cargo_set_toolchain', {'which': 'project_variant', 'variant': 'bench', 'toolchain': 'nightly'}) self._run_build_wait('bench') @@ -288,8 +290,9 @@ def test_clippy(self): self._test_clippy) def _test_clippy(self, view): + self._cargo_clean(view) window = view.window() - window.run_command('cargo_set_toolchain', {'which': 'variant', + window.run_command('cargo_set_toolchain', {'which': 'project_variant', 'variant': 'clippy', 'toolchain': 'nightly'}) self._run_build_wait('clippy') @@ -329,28 +332,28 @@ def _test_features(self, view): output = self._get_build_output(window) self.assertRegex(output, '(?m)^feats: feat1$') - window.run_command('cargo_set_features', {'target': None, + window.run_command('cargo_set_features', {'which': 'project_package_default', 'no_default_features': True, 'features': ''}) self._run_build_wait('run') output = self._get_build_output(window) self.assertRegex(output, '(?m)^feats: $') - window.run_command('cargo_set_features', {'target': None, + window.run_command('cargo_set_features', {'which': 'project_package_default', 'no_default_features': False, 'features': 'feat3'}) self._run_build_wait('run') output = self._get_build_output(window) self.assertRegex(output, '(?m)^feats: feat1 feat3$') - window.run_command('cargo_set_features', {'target': None, + window.run_command('cargo_set_features', {'which': 'project_package_default', 'no_default_features': True, 'features': 'feat2 feat3'}) self._run_build_wait('run') output = self._get_build_output(window) self.assertRegex(output, '(?m)^feats: feat2 feat3$') - window.run_command('cargo_set_features', {'target': None, + window.run_command('cargo_set_features', {'which': 'project_package_default', 'no_default_features': True, 'features': 'ALL'}) self._run_build_wait('run') @@ -380,8 +383,8 @@ def _test_build_env(self, view): window = view.window() settings = cargo_settings.CargoSettings(window) settings.load() - settings.set_with_target(multi_target_root, '--bin penv', 'env', - {'RUST_BUILD_ENV_TEST': 'abcdef'}) + settings.set_project_package_target(multi_target_root, '--bin penv', + 'env', {'RUST_BUILD_ENV_TEST': 'abcdef'}) window.run_command('cargo_set_target', {'variant': 'run', 'target': '--bin penv'}) self._run_build_wait('run') diff --git a/tests/test_cargo_settings.py b/tests/test_cargo_settings.py new file mode 100644 index 00000000..19d368b7 --- /dev/null +++ b/tests/test_cargo_settings.py @@ -0,0 +1,56 @@ +"""Tests for configuration settings and Cargo build.""" + +import os + +from rust_test_common import * + + +class TestCargoSettings(TestBase): + + def test_settings(self): + window = sublime.active_window() + cmd_info = cargo_settings.CARGO_COMMANDS['build'] + manifest_dir = os.path.join(plugin_path, 'tests', 'multi-targets') + settings = cargo_settings.CargoSettings(window) + settings.load() + + def check_cmd(expected_cmd): + cmd = settings.get_command('build', + cmd_info, manifest_dir)['command'] + self.assertEqual(cmd, expected_cmd.split()) + + cmd = 'cargo build --message-format=json' + check_cmd(cmd) + + cb = {'defaults': {'extra_cargo_args': 'global_args'}} + self._override_setting('cargo_build', cb) + check_cmd(cmd + ' global_args') + + settings.set_project_default('extra_cargo_args', 'project_defaults') + check_cmd(cmd + ' project_defaults') + + cb['variants'] = {'build': {'extra_cargo_args': 'global_var_args'}} + self._override_setting('cargo_build', cb) + check_cmd(cmd + ' global_var_args') + + settings.set_project_variant('build', + 'extra_cargo_args', 'project_var_args') + check_cmd(cmd + ' project_var_args') + + settings.set_project_package_default(manifest_dir, + 'extra_cargo_args', 'proj_pack_def_arg') + check_cmd(cmd + ' proj_pack_def_arg') + + settings.set_project_package_variant(manifest_dir, 'build', + 'extra_cargo_args', 'proj_pack_var_arg') + check_cmd(cmd + ' proj_pack_var_arg') + + settings.set_project_package_target(manifest_dir, '--example ex1', + 'extra_cargo_args', 'proj_pack_target_args') + # Does not change. + check_cmd(cmd + ' proj_pack_var_arg') + + # Change the default target. + settings.set_project_package_variant(manifest_dir, 'build', 'target', + '--example ex1') + check_cmd('cargo build --example ex1 --message-format=json proj_pack_target_args')