cargo_plugin.py 9.3 KB
Newer Older
1
2
3
4
5
#!/usr/bin/env python3

#
# cargo_plugin.py
#
6
# Copyright 2016 Christian Hergert <chris@dronelabs.com>
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import threading
23
import os
24
25
26
27
28
29

from gi.repository import Gio
from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Ide

30
31
_ = Ide.gettext

32
_CARGO = 'cargo'
33
34
35
36
37
_ERROR_FORMAT_REGEX = ("(?<filename>[a-zA-Z0-9\\+\\-\\.\\/_]+):"
                       "(?<line>\\d+):"
                       "(?<column>\\d+): "
                       "(?<level>[\\w\\[a-zA-Z0-9\\]\\s]+): "
                       "(?<message>.*)")
38

39
40
41
42
43
44
45
46
class CargoBuildSystemDiscovery(Ide.SimpleBuildSystemDiscovery):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.props.glob = 'Cargo.toml'
        self.props.hint = 'cargo_plugin'
        self.props.priority = -200

class CargoBuildSystem(Ide.Object, Ide.BuildSystem):
47
48
    project_file = GObject.Property(type=Gio.File)

49
    def do_get_id(self):
50
51
        return 'cargo'

52
53
54
    def do_get_display_name(self):
        return 'Cargo'

55
    def do_get_priority(self):
56
        return -200
57

58
59
60
61
62
63
64
65
66
67
68
69
70
71
def locate_cargo_from_config(config):
    cargo = _CARGO

    if config:
        runtime = config.get_runtime()
        if config.getenv('CARGO'):
            cargo = config.getenv('CARGO')
        elif not runtime or not runtime.contains_program_in_path(_CARGO):
            cargo_in_home = os.path.expanduser('~/.cargo/bin/cargo')
            if os.path.exists(cargo_in_home):
                cargo = cargo_in_home

    return cargo

72
class CargoPipelineAddin(Ide.Object, Ide.PipelineAddin):
73
74
75
76
    """
    The CargoPipelineAddin is responsible for creating the necessary build
    stages and attaching them to phases of the build pipeline.
    """
77

78
    def do_load(self, pipeline):
79
        context = self.get_context()
80
        build_system = Ide.BuildSystem.from_context(context)
81

82
        # Always register the error regex
83
84
85
        self.error_format_id = pipeline.add_error_format(_ERROR_FORMAT_REGEX,
                                                         GLib.RegexCompileFlags.OPTIMIZE |
                                                         GLib.RegexCompileFlags.CASELESS);
86

87
88
89
90
91

        # Ignore pipeline unless this is a cargo project
        if type(build_system) != CargoBuildSystem:
            return

92
93
94
95
96
        project_file = build_system.props.project_file
        if project_file.get_basename() != 'Cargo.toml':
            project_file = project_file.get_child('Cargo.toml')

        cargo_toml = project_file.get_path()
97
        config = pipeline.get_config()
98
        builddir = pipeline.get_builddir()
99
100
101
        runtime = config.get_runtime()

        # We might need to use cargo from ~/.cargo/bin
102
        cargo = locate_cargo_from_config(config)
103
104
105
106

        # Fetch dependencies so that we no longer need network access
        fetch_launcher = pipeline.create_launcher()
        fetch_launcher.setenv('CARGO_TARGET_DIR', builddir, True)
107
        fetch_launcher.push_argv(cargo)
108
109
110
        fetch_launcher.push_argv('fetch')
        fetch_launcher.push_argv('--manifest-path')
        fetch_launcher.push_argv(cargo_toml)
111
        self.track(pipeline.attach_launcher(Ide.PipelinePhase.DOWNLOADS, 0, fetch_launcher))
112

Christian Hergert's avatar
Christian Hergert committed
113
        # Now create our launcher to build the project
114
115
        build_launcher = pipeline.create_launcher()
        build_launcher.setenv('CARGO_TARGET_DIR', builddir, True)
116
        build_launcher.push_argv(cargo)
117
        build_launcher.push_argv('rustc')
118
119
120
121
122
        build_launcher.push_argv('--manifest-path')
        build_launcher.push_argv(cargo_toml)
        build_launcher.push_argv('--message-format')
        build_launcher.push_argv('human')

123
        if not pipeline.is_native():
124
            build_launcher.push_argv('--target')
125
            build_launcher.push_argv(pipeline.get_host_triplet().get_full_name())
126
127
128
129
130
131
132

        if config.props.parallelism > 0:
            build_launcher.push_argv('-j{}'.format(config.props.parallelism))

        if not config.props.debug:
            build_launcher.push_argv('--release')

133
134
135
136
137
138
        build_launcher.push_argv('--')
        build_launcher.push_argv('--error-format')
        build_launcher.push_argv('short')
        build_launcher.push_argv('--remap-path-prefix')
        build_launcher.push_argv('=' + pipeline.get_srcdir())

139
140
        clean_launcher = pipeline.create_launcher()
        clean_launcher.setenv('CARGO_TARGET_DIR', builddir, True)
141
        clean_launcher.push_argv(cargo)
142
143
144
145
        clean_launcher.push_argv('clean')
        clean_launcher.push_argv('--manifest-path')
        clean_launcher.push_argv(cargo_toml)

146
        build_stage = Ide.PipelineStageLauncher.new(context, build_launcher)
147
        build_stage.set_name(_("Building project"))
148
        build_stage.set_clean_launcher(clean_launcher)
149
        build_stage.connect('query', self._query)
150
        self.track(pipeline.attach(Ide.PipelinePhase.BUILD, 0, build_stage))
151

152
153
154
155
    def do_unload(self, pipeline):
        if self.error_format_id:
            pipeline.remove_error_format(self.error_format_id)

156
    def _query(self, stage, pipeline, targets, cancellable):
157
158
159
        # Always defer to cargo to check if build is needed
        stage.set_completed(False)

160
161
162
163
164
class CargoBuildTarget(Ide.Object, Ide.BuildTarget):

    def do_get_install_directory(self):
        return None

165
166
167
    def do_get_display_name(self):
        return 'cargo run'

168
169
170
    def do_get_name(self):
        return 'cargo-run'

171
172
173
    def do_get_language(self):
        return 'rust'

174
175
    def do_get_argv(self):
        context = self.get_context()
176
        config_manager = Ide.ConfigManager.from_context(context)
177
178
179
180
181
        config = config_manager.get_current()
        cargo = locate_cargo_from_config(config)

        # Pass the Cargo.toml path so that we don't
        # need to run from the project directory.
182
        cargo_toml = context.ref_workdir().get_child('Cargo.toml')
183
184
185
186
187
188
189
190
191
192
193
194
195

        return [cargo, 'run', '--manifest-path', cargo_toml]

    def do_get_priority(self):
        return 0

class CargoBuildTargetProvider(Ide.Object, Ide.BuildTargetProvider):

    def do_get_targets_async(self, cancellable, callback, data):
        task = Gio.Task.new(self, cancellable, callback)
        task.set_priority(GLib.PRIORITY_LOW)

        context = self.get_context()
196
        build_system = Ide.BuildSystem.from_context(context)
197
198
199
200
201
202
203

        if type(build_system) != CargoBuildSystem:
            task.return_error(GLib.Error('Not cargo build system',
                                         domain=GLib.quark_to_string(Gio.io_error_quark()),
                                         code=Gio.IOErrorEnum.NOT_SUPPORTED))
            return

204
        task.targets = [build_system.ensure_child_typed(CargoBuildTarget)]
205
206
207
208
209
        task.return_boolean(True)

    def do_get_targets_finish(self, result):
        if result.propagate_boolean():
            return result.targets
210
211
212
213
214
215
216
217

class CargoDependencyUpdater(Ide.Object, Ide.DependencyUpdater):

    def do_update_async(self, cancellable, callback, data):
        task = Gio.Task.new(self, cancellable, callback)
        task.set_priority(GLib.PRIORITY_LOW)

        context = self.get_context()
218
        build_system = Ide.BuildSystem.from_context(context)
219
220
221
222
223
224

        # Short circuit if not using cargo
        if type(build_system) != CargoBuildSystem:
            task.return_boolean(True)
            return

225
        build_manager = Ide.BuildManager.from_context(context)
226
227
228
229
230
231
232
        pipeline = build_manager.get_pipeline()
        if not pipeline:
            task.return_error(GLib.Error('Cannot update dependencies without build pipeline',
                                         domain=GLib.quark_to_string(Gio.io_error_quark()),
                                         code=Gio.IOErrorEnum.FAILED))
            return

233
        config_manager = Ide.ConfigManager.from_context(context)
234
235
236
        config = config_manager.get_current()
        cargo = locate_cargo_from_config(config)

237
238
239
240
        project_file = build_system.props.project_file
        if project_file.get_basename() != 'Cargo.toml':
            project_file = project_file.get_child('Cargo.toml')
        cargo_toml = project_file.get_path()
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263

        launcher = pipeline.create_launcher()
        launcher.setenv('CARGO_TARGET_DIR', pipeline.get_builddir(), True)
        launcher.push_argv(cargo)
        launcher.push_argv('update')
        launcher.push_argv('--manifest-path')
        launcher.push_argv(cargo_toml)

        try:
            subprocess = launcher.spawn()
            subprocess.wait_check_async(None, self.wait_check_cb, task)
        except Exception as ex:
            task.return_error(ex)

    def do_update_finish(self, result):
        return result.propagate_boolean()

    def wait_check_cb(self, subprocess, result, task):
        try:
            subprocess.wait_check_finish(result)
            task.return_boolean(True)
        except Exception as ex:
            task.return_error(ex)