summaryrefslogtreecommitdiffstats
path: root/debian/bin/cargo
blob: 50772347d09c5a8d47f8c3f3414eee3305bfede1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
#!/usr/bin/python3
"""
Wrapper around cargo to have it build using Debian settings.

Usage:
    export PATH=/path/to/dir/of/this/script:$PATH
    export CARGO_HOME=debian/cargo_home
    cargo prepare-debian /path/to/local/registry [--link-from-system]
    cargo build
    cargo test
    cargo install
    cargo clean
    [rm -rf /path/to/local/registry]

The "prepare-debian" subcommand writes a config file to $CARGO_HOME that makes
the subsequent invocations use our Debian flags. The "--link-from-system" flag
is optional; if you use it we will create /path/to/local/registry and symlink
the contents of /usr/share/cargo/registry into it. You are then responsible for
cleaning it up afterwards (a simple `rm -rf` should do).

See cargo:d/rules and dh-cargo:cargo.pm for more examples.

Make sure you add "Build-Depends: python3:native" if you use this directly.
If using this only indirectly via dh-cargo, then you only need "Build-Depends:
dh-cargo"; this is a general principle when declaring dependencies.

If CARGO_HOME doesn't end with debian/cargo_home, then this script does nothing
and passes through directly to cargo.

Otherwise, you *must* set the following environment variables:

- DEB_CARGO_CRATE
  ${crate}_${version} of whatever you're building.

- CFLAGS CXXFLAGS CPPFLAGS LDFLAGS [*]
- DEB_HOST_GNU_TYPE DEB_HOST_RUST_TYPE [*]

- (required only for `cargo install`) DESTDIR
  DESTDIR to install build artifacts under. If running via dh-cargo, this will
  be set automatically by debhelper, see `dh_auto_install` for details.

- (optional) DEB_BUILD_OPTIONS DEB_BUILD_PROFILES

- (optional) DEB_CARGO_INSTALL_PREFIX
  Prefix to install build artifacts under. Default: /usr. Sometimes you might
  want to change this to /usr/lib/cargo if the binary clashes with something
  else, and then symlink it into /usr/bin under an alternative name.

- (optional) DEB_CARGO_CRATE_IN_REGISTRY
  Whether the crate is in the local-registry (1) or cwd (0, empty, default).

For the envvars marked [*], it is easiest to set these in your d/rules via:

    include /usr/share/dpkg/architecture.mk
    include /usr/share/dpkg/buildflags.mk
    include /usr/share/rustc/architecture.mk
    export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
    export DEB_HOST_RUST_TYPE DEB_HOST_GNU_TYPE
"""

import os
import os.path
import shutil
import subprocess
import sys

FLAGS = "CFLAGS CXXFLAGS CPPFLAGS LDFLAGS"
ARCHES = "DEB_HOST_GNU_TYPE DEB_HOST_RUST_TYPE"
SYSTEM_REGISTRY = "/usr/share/cargo/registry"

def log(*args):
    print("debian cargo wrapper:", *args, file=sys.stderr, flush=True)

def logrun(*args, **kwargs):
    log("running subprocess", args, kwargs)
    return subprocess.run(*args, **kwargs)

def sourcepath(p=None):
    return os.path.join(os.getcwd(), p) if p else os.getcwd()

def prepare_debian(cargo_home, registry, cratespec, host_gnu_type, ldflags, link_from_system, extra_rustflags):
    registry_path = sourcepath(registry)
    if link_from_system:
        log("linking %s/* into %s/" % (SYSTEM_REGISTRY, registry_path))
        os.makedirs(registry_path, exist_ok=True)
        crates = os.listdir(SYSTEM_REGISTRY) if os.path.isdir(SYSTEM_REGISTRY) else []
        for c in crates:
            target = os.path.join(registry_path, c)
            if not os.path.islink(target):
                os.symlink(os.path.join(SYSTEM_REGISTRY, c), target)
    elif not os.path.exists(registry_path):
        raise ValueError("non-existent registry: %s" % registry)

    rustflags = "-C debuginfo=2 --cap-lints warn".split()
    rustflags.extend(["-C", "linker=%s-gcc" % host_gnu_type])
    for f in ldflags:
        rustflags.extend(["-C", "link-arg=%s" % f])
    if link_from_system:
        rustflags.extend([
            # Note that this order is important! Rust evaluates these options in
            # priority of reverse order, so if the second option were in front,
            # it would never be used, because any paths in registry_path are
            # also in sourcepath().
            "--remap-path-prefix", "%s=%s/%s" %
                (sourcepath(), SYSTEM_REGISTRY, cratespec.replace("_", "-")),
            "--remap-path-prefix", "%s=%s" % (registry_path, SYSTEM_REGISTRY),
        ])
    rustflags.extend(extra_rustflags.split())

    # TODO: we cannot enable this until dh_shlibdeps works correctly; atm we get:
    # dpkg-shlibdeps: warning: can't extract name and version from library name 'libstd-XXXXXXXX.so'
    # and the resulting cargo.deb does not depend on the correct version of libstd-rust-1.XX
    # We probably need to add override_dh_makeshlibs to d/rules of rustc
    #rustflags.extend(["-C", "prefer-dynamic"])

    os.makedirs(cargo_home, exist_ok=True)
    with open("%s/config" % cargo_home, "w") as fp:
        fp.write("""[source.crates-io]
replace-with = "dh-cargo-registry"

[source.dh-cargo-registry]
directory = "{0}"

[build]
rustflags = {1}
""".format(registry_path, repr(rustflags)))

    return 0

def install(destdir, cratespec, host_rust_type, crate_in_registry, install_prefix, *args):
    crate, version = cratespec.rsplit("_", 1)
    log("installing into destdir '%s' prefix '%s'" % (destdir, install_prefix))
    install_target = destdir + install_prefix
    logrun(["env", "RUST_BACKTRACE=1",
        # set CARGO_TARGET_DIR so build products are saved in target/
        # normally `cargo install` deletes them when it exits
        "CARGO_TARGET_DIR=" + sourcepath("target"),
        "/usr/bin/cargo"] + list(args) +
        ([crate, "--vers", version] if crate_in_registry else ["--path", sourcepath()]) +
        ["--root", install_target], check=True)
    logrun(["rm", "-f", "%s/.crates.toml" % install_target])
    logrun(["rm", "-f", "%s/.crates2.json" % install_target])

    # if there was a custom build output, symlink it to debian/cargo_out_dir
    # hopefully cargo will provide a better solution in future https://github.com/rust-lang/cargo/issues/5457
    r = logrun('''ls -td "target/%s/release/build/%s"-*/out 2>/dev/null | head -n1'''
        % (host_rust_type, crate), shell=True, stdout=subprocess.PIPE).stdout
    r = r.decode("utf-8").rstrip()
    if r:
        logrun(["ln", "-sfT", "../%s" % r, "debian/cargo_out_dir"], check=True)
    return 0

def main(*args):
    cargo_home = os.getenv("CARGO_HOME", "")
    if not cargo_home.endswith("/debian/cargo_home"):
        os.execv("/usr/bin/cargo", ["cargo"] + list(args))

    if any(f not in os.environ for f in FLAGS.split()):
        raise ValueError("not all of %s set; did you call dpkg-buildflags?" % FLAGS)

    if any(f not in os.environ for f in ARCHES.split()):
        raise ValueError("not all of %s set; did you include architecture.mk?" % ARCHES)

    build_options = os.getenv("DEB_BUILD_OPTIONS", "").split()
    build_profiles = os.getenv("DEB_BUILD_PROFILES", "").split()

    parallel = []
    lto = 0
    for o in build_options:
        if o.startswith("parallel="):
            parallel = ["-j" + o[9:]]
        elif o.startswith("optimize="):
            opt_arg = o[9:]
            for arg in opt_arg.split(","):
                if opt_arg == "-lto":
                    lto = -1
                elif opt_arg == "+lto":
                    lto = 1
                else:
                    log(f"WARNING: unhandled optimization flag: {opt_arg}")

    nodoc = "nodoc" in build_options or "nodoc" in build_profiles
    nocheck = "nocheck" in build_options or "nocheck" in build_profiles

    # note this is actually the "build target" type, see rustc's README.Debian
    # for full details of the messed-up terminology here
    host_rust_type = os.getenv("DEB_HOST_RUST_TYPE", "")
    host_gnu_type = os.getenv("DEB_HOST_GNU_TYPE", "")

    log("options, profiles, parallel, lto:", build_options, build_profiles, parallel, lto)
    log("rust_type, gnu_type:", ", ".join([host_rust_type, host_gnu_type]))

    if "RUSTFLAGS" in os.environ:
        # see https://github.com/rust-lang/cargo/issues/6338 for explanation on why we must do this
        log("unsetting RUSTFLAGS and assuming it will be (or already was) added to $CARGO_HOME/config")
        extra_rustflags = os.environ["RUSTFLAGS"]
        del os.environ["RUSTFLAGS"]
    else:
        extra_rustflags = ""

    if args[0] == "prepare-debian":
        registry = args[1]
        link_from_system = False
        if len(args) > 2 and args[2] == "--link-from-system":
            link_from_system = True
        return prepare_debian(cargo_home, registry,
            os.environ["DEB_CARGO_CRATE"], host_gnu_type,
            os.getenv("LDFLAGS", "").split(), link_from_system, extra_rustflags)

    newargs = []
    subcmd = None
    for a in args:
        if (subcmd is None) and (a in ("build", "rustc", "doc", "test", "bench", "install")):
            subcmd = a
            newargs.extend(["-Zavoid-dev-deps", a, "--verbose", "--verbose"] +
                parallel + ["--target", host_rust_type])
        elif (subcmd is None) and (a == "clean"):
            subcmd = a
            newargs.extend([a, "--verbose", "--verbose"])
        else:
            newargs.append(a)

    if subcmd is not None and "--verbose" in newargs and "--quiet" in newargs:
        newargs.remove("--quiet")

    if nodoc and subcmd == "doc":
        return 0
    if nocheck and subcmd in ("test", "bench"):
        return 0

    if lto == 1:
        newargs.append("--config profile.release.lto = \"thin\"")
    elif lto == -1:
        newargs.append("--config profile.release.lto = false")

    if subcmd == "clean":
        logrun(["env", "RUST_BACKTRACE=1", "/usr/bin/cargo"] + list(newargs), check=True)
        if os.path.exists(cargo_home):
            shutil.rmtree(cargo_home)
        return 0

    cargo_config = "%s/config" % cargo_home
    if not os.path.exists(cargo_config):
        raise ValueError("does not exist: %s, did you run `cargo prepare-debian <registry>`?" % cargo_config)

    if subcmd == "install":
        return install(os.getenv("DESTDIR", ""),
            os.environ["DEB_CARGO_CRATE"],
            host_rust_type,
            os.getenv("DEB_CARGO_CRATE_IN_REGISTRY", "") == "1",
            os.getenv("DEB_CARGO_INSTALL_PREFIX", "/usr"),
            *newargs)
    else:
        return logrun(["env", "RUST_BACKTRACE=1", "/usr/bin/cargo"] + list(newargs)).returncode

if __name__ == "__main__":
    sys.exit(main(*sys.argv[1:]))