Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add comprehensive support for remote docker. #785

Merged
merged 4 commits into from
Jun 23, 2022
Merged

Conversation

Alexhuszagh
Copy link
Contributor

@Alexhuszagh Alexhuszagh commented Jun 12, 2022

Add comprehensive support for remote docker.

This supports the volume-based structure, and uses some nice optimizations to ensure that only the desired toolchain and cargo items are copied over. It also uses drops to ensure scoped deletion of resources, to avoid complex logic ensuring their cleanup.

It also supports persistent data volumes, through cross-util. In order to setup a persistent data volume, use:

cross-util volumes create

This will create a persistent data volume specific for your current toolchain, and will be shared by all targets. This volume is specific for the toolchain version and channel, so a new volume will be created for each toolchain.

Make sure you provide your DOCKER_HOST or correct engine type to ensure these are being made on the remote host. Then, run your command as before:

CROSS_REMOTE=1 cross build --target arm-unknown-linux-gnueabihf

Finally, you can clean up the generated volume using:

cross-util volumes remove

A few other utilities are present in cross-util:

  • volumes list: list all volumes created by cross.
  • volumes remove: remove all volumes created by cross.
  • volumes prune: prune all volumes unassociated with a container.
  • containers list: list all active containers created by cross.
  • containers remove: remove all active containers created by cross.
  • clean: clean all temporary data (images, containers, volumes, temp data) created by cross.

The initial implementation was done by Marc Schreiber, schrieveslaach.

A few more environment variables exist to fine-tune performance, as well as handle private dependencies.

  • CROSS_REMOTE_COPY_REGISTRY: copy the cargo registry
  • CROSS_REMOTE_COPY_CACHE: copy cache directories, including the target directory.

Both of these generally lead to substantial performance penalties, but can enable the use of private SSH dependencies. In either case, the use of persistent data volumes is highly recommended.

Fixes #248.
Fixes #273.
Closes #449.

@Alexhuszagh Alexhuszagh requested a review from a team as a code owner June 12, 2022 23:37
@Alexhuszagh
Copy link
Contributor Author

Alexhuszagh commented Jun 12, 2022

This is a massive PR, so absolutely no rush, and I'll likely be incrementally improving the code quality, but reviews are greatly appreciated, as our beta-testers.

@Alexhuszagh Alexhuszagh force-pushed the remote branch 3 times, most recently from 1951761 to 3812d6d Compare June 12, 2022 23:44
@Alexhuszagh
Copy link
Contributor Author

Alexhuszagh commented Jun 12, 2022

I believe this might break private SSH dependency support, but I'm not sure if there's a great solution to this anyway. Maybe copying over the entire registry would work?

src/docker.rs Outdated Show resolved Hide resolved
src/docker.rs Outdated Show resolved Hide resolved
@Emilgardis Emilgardis self-assigned this Jun 13, 2022
@Alexhuszagh Alexhuszagh marked this pull request as draft June 13, 2022 00:54
@Emilgardis
Copy link
Member

this is great!

will have to take some time to take it all in

src/docker.rs Outdated Show resolved Hide resolved
@Alexhuszagh
Copy link
Contributor Author

Alexhuszagh commented Jun 13, 2022

Just a few things as considerations:

  1. I currently skip the cargo registry, as this is generally a good idea, however, it might break private SSH dependency support. This could be enabled with an environment variable (I was previously using CROSS_REMOTE_COPY_REGISTRY.
  2. The target directory gets to be quite large, and this can often be slower than doing a full rebuild. It's easy to identify anything that should not be copied over, since Rust uses cache directory tagging. For example, a project I had not run cargo clean on in 4 years had target directory size of 90GB.

Basically, a config file or 2 environment variable settings saying:

  • CROSS_REMOTE_COPY_REGISTRY: do copy the registry
  • CROSS_REMOTE_COPY_CACHE: do copy cache files, including the target directory

Thoughts? Good idea? Default both of these to off, but enable their use?

@Alexhuszagh Alexhuszagh force-pushed the remote branch 6 times, most recently from 2c8b1a5 to f99be56 Compare June 13, 2022 19:42
@Alexhuszagh
Copy link
Contributor Author

Alexhuszagh commented Jun 13, 2022

Ok I've added the registry support and cache support, which should further speed up execution time.

Not setting CROSS_REMOTE_COPY_CACHE will effectively disable incremental compilation unless a persistent volume is used, which in most cases is still probably faster unless doing a release build.

Setting CROSS_REMOTE_COPY_REGISTRY will also copy the entire registry, which should allow the use of private SSH dependencies.

Both are disabled by default, since on a remote machine it can be quite slow. We use a temporary directory to store the files when copying only partial directories, to avoid too many calls to docker (quite slow).

Copy link
Member

@Emilgardis Emilgardis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the files are getting rather large, could you split out the remote support stuff into it's own module, e.g inside docker/remote.rs

src/bin/cross-util.rs Outdated Show resolved Hide resolved
@Alexhuszagh Alexhuszagh force-pushed the remote branch 3 times, most recently from 4839df9 to dccae12 Compare June 14, 2022 15:48
@Alexhuszagh
Copy link
Contributor Author

I've refactored the codebase more thoroughly:

  1. docker is now a directory, with the remote, local, shared, and engine submodules.
  2. commands is now in /src/bin.
  3. Since we now glob import from docker, I've prefixed a few of the names.
  4. Another thing I've fixed (which should be a separate PR) is fixed docker_in_docker support, which currently expects a hard-coded docker and can just use the engine value we've already gotten.

This should help isolated the behavior better between local and shared, and help

@Alexhuszagh Alexhuszagh force-pushed the remote branch 2 times, most recently from 470e5ac to 54f2717 Compare June 14, 2022 20:43
@Alexhuszagh
Copy link
Contributor Author

I've rebased after #794 and updated everything to use the remote options if CROSS_REMOTE is active. I'm not sure if building images will work if that's the case, but it's likely better to error out than try to build them on the local machine if they're trying to use a remote host.

@Alexhuszagh
Copy link
Contributor Author

Alexhuszagh commented Jun 21, 2022

So a few things have been resolved in the latest commit:

  1. The persistent data volume can support more than 1 target. It does this by checking if the target directory in rustlib already exists, and if it does, doesn't copy over the files. If it does not exist, it copies over the files and recopies all the manifest files as well. This is a pretty cheap check, and reduces volume size a lot.
  2. The volume names have gotten a lot nicer, for example, cross-stable-x86_64-unknown-linux-gnu-16b8c-fe5b13d68. Here, we have cross-{toolchain_name}-{sysroot_hash}-{rustc_commit}, where toolchain_name is the filename of the toolchain sysroot, toolchain_hash is the hash of the path to the sysroot, and toolchain_commit is the 9 hex-digit commit identifier of the rustc version used. If we use a non-persistent data volume, we have cross-{toolchain_name}-{sysroot_hash}-{rustc_commit-{triple}-{name}-{project_hash}, where triple is the target triple, name is the project name, and project_hash is the hash of the path to the project root. This means everything is unique, with minimal chance for conflict.

Giving the cross-stable-x86_64-unknown-linux-gnu-... really makes the volume identifier much more human-friendly, since the rest is unique identifiers, and this provides a good way for the user to know what volume corresponds to what toolchain.

However.... #724 is reproducing, and without a way to run cargo clean remotely, there's no way to fix this besides just deleting the data volume currently. So we probably need to add cargo clean into the list of commands that runs in the container, at least remotely, and this can be quite a showstopper.

Other than that, I'll test this more but I think it's ready.

@Alexhuszagh
Copy link
Contributor Author

Alexhuszagh commented Jun 21, 2022

For cargo clean, it actually only makes sense to run in the image if all the conditions are met, and then it still needs to be run outside the container:

  • Persistent data volume
  • Is remote

Since it's a persistent data volume and we need to copy the data back to the host to make any changes, merely cleaning it in the volume won't be enough on its own, so we should do both.

So it's worth adding it as a subcommand, but only if all those conditions are met also running it in the container, and then also falling back to the host. So I'll add this as a separate commit to the PR.

@Emilgardis
Copy link
Member

why do we need to run cargo clean on the host as well?

@Alexhuszagh
Copy link
Contributor Author

Alexhuszagh commented Jun 21, 2022

why do we need to run cargo clean on the host as well?

Because it copies the data back over to the host, although I guess we could skip this unless the copy cache is set, since a primary use of cross is to distribute cross-compiled binaries.

I guess we could always make this a cross-util command?

@Emilgardis
Copy link
Member

was mostly wondering, we copy over the target folder so makes sense to me to clean on the host.

@Alexhuszagh Alexhuszagh changed the title Initial commit with basic support for remote docker. Add comprehensive support for remote docker. Jun 22, 2022
@Alexhuszagh
Copy link
Contributor Author

Alexhuszagh commented Jun 22, 2022

This is ready for review, and adds support for cargo clean on the remote host as well. I've tested it works on:

  • Windows (not WSL2)
  • Linux
  • macOS

A few things we can add to our wishlist later:

  • Only copy source/project files that have a changed timestamp to minimize network traffic on incremental builds. This would require using some sort of memo like Rust does.
  • Tracing for the tempfiles created.
  • Deleting files that have been removed (which this currently doesn't do), which might be an issue in complex build scripts that checking files in a directory (like cross itself).

It might be safer currently to just delete the project directory, and then always copy over all the files, so it always works? This would also mean if the target directory is a subdirectory of the project, there are no incremental builds are therefore they are significantly slower.

Create and robustly delete temporary data, which is stored in the user data directory under `${data_dir}/cross-rs/tmp`. Therefore, if program execution is ever halted, deleting the directory will automatically cleanup any remaining data.

A termination handler cleans up the temporary data on exit by clearing a stack of temporary files/directories, and we have 2 resource handlers that cleanup the temporary data when the object is dropped.

The new installer for the startup hooks is `install_exit_hooks`, which installs both the panic and termination handlers. To get the directory containing all temporary data, use `cross::temp::dir()`.

An example of using temporary directories and files is:

```rust
{
    // these are all safe due to single-threaded execution
    let tmpdir1 = unsafe { temp::TempFile::new() }?;
    let tmpdir2 = unsafe { temp::TempFile::new() }?;
    let tmpfile1 = unsafe { temp::TempFile::new() }?;
    let tmpfile2 = unsafe { temp::TempFile::new() }?;

    for entry in fs::read_dir(tmpdir1.path()) {
        ...
    }
    for entry in fs::read_dir(tmpdir2.path()) {
        ...
    }
}   // cleanup tmpfile2 -> tmpfile1 -> tmpdir2 -> tmpdir1
```

Note that only these 2 wrappers are provided, since it guarantees the temporary file and directory stack stays ordered and resources are cleaned up as desired.
This supports the volume-based structure, and uses some nice
optimizations to ensure that only the desired toolchain and cargo items
are copied over. It also uses drops to ensure scoped deletion of
resources, to avoid complex logic ensuring their cleanup.

It also supports persistent data volumes, through `cross-util`. In order to setup a persistent data volume, use:

```bash
cross-util volumes create
```

This will create a persistent data volume specific for your current toolchain, and will be shared by all targets. This volume is specific for the toolchain version and channel, so a new volume will be created for each toolchain.

Make sure you provide your `DOCKER_HOST` or correct engine type to ensure these are being made on the remote host. Then, run your command as before:

```bash
CROSS_REMOTE=1 cross build --target arm-unknown-linux-gnueabihf
```

Finally, you can clean up the generated volume using:

```bash
cross-util volumes remove
```

A few other utilities are present in `cross-util`:
- `volumes list`: list all volumes created by cross.
- `volumes remove`: remove all volumes created by cross.
- `volumes prune`: prune all volumes unassociated with a container.
- `containers list`: list all active containers created by cross.
- `containers remove`: remove all active containers created by cross.
- `clean`: clean all temporary data (images, containers, volumes, temp data) created by cross.

The initial implementation was done by Marc Schreiber, schrieveslaach.

A few more environment variables exist to fine-tune performance, as well as handle private dependencies.
- `CROSS_REMOTE_COPY_REGISTRY`: copy the cargo registry
- `CROSS_REMOTE_COPY_CACHE`: copy cache directories, including the target directory.

Both of these generally lead to substantial performance penalties, but can enable the use of private SSH dependencies. In either case, the use of persistent data volumes is highly recommended.

Fixes cross-rs#248.
Fixes cross-rs#273.
Closes cross-rs#449.
Required to patch cross-rs#724 without deleting the entire volume for persistent
data volumes. A few changes were required: the entire `/cross` mount
prefix must be owned by the user so `/cross/target` can be removed.
Next, we use the full path to the mounted target directory, rather than
the symlink, since `cargo clean` would just delete the symlink. Finally,
we've added `cargo clean` to a list of known subcommands, and it only
needs docker if we use a remote host.
Updates the persistent volume using a fingerprint of all files in the
project, skipping any cache directories by default. If the file modified
date has changed, or the file has been added, copy it to the volume and
update it. If the file has been removed, then remove it from the host.
To avoid a massive command-line argument, we copy a file containing each
changed file on a line to the container, and then remove each file
by running a script on the container.
Copy link
Member

@Emilgardis Emilgardis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

I think this all looks good, one todo is trying to figure out if we're in remote context and spit out a nice hint to use CROSS_REMOTE.

feel free to r= me

src/rustc.rs Show resolved Hide resolved
@Alexhuszagh
Copy link
Contributor Author

Awesome!

I think this all looks good, one todo is trying to figure out if we're in remote context and spit out a nice hint to use CROSS_REMOTE.

feel free to r= me

That might be good, it can be quite tricky to unambiguously determine if we in a remote context, but it incurs some seriously overhead (see the comment here), which might matter when normally running on localhost.

@Alexhuszagh
Copy link
Contributor Author

bors r=Emilgardis

bors bot added a commit that referenced this pull request Jun 23, 2022
785: Add comprehensive support for remote docker. r=Emilgardis a=Alexhuszagh

Add comprehensive support for remote docker.

This supports the volume-based structure, and uses some nice optimizations to ensure that only the desired toolchain and cargo items are copied over. It also uses drops to ensure scoped deletion of resources, to avoid complex logic ensuring their cleanup.

It also supports persistent data volumes, through `cross-util`. In order to setup a persistent data volume, use:

```bash
cross-util volumes create
```

This will create a persistent data volume specific for your current toolchain, and will be shared by all targets. This volume is specific for the toolchain version and channel, so a new volume will be created for each toolchain.

Make sure you provide your `DOCKER_HOST` or correct engine type to ensure these are being made on the remote host. Then, run your command as before:

```bash
CROSS_REMOTE=1 cross build --target arm-unknown-linux-gnueabihf
```

Finally, you can clean up the generated volume using:

```bash
cross-util volumes remove
```

A few other utilities are present in `cross-util`:
- `volumes list`: list all volumes created by cross.
- `volumes remove`: remove all volumes created by cross.
- `volumes prune`: prune all volumes unassociated with a container.
- `containers list`: list all active containers created by cross.
- `containers remove`: remove all active containers created by cross.
- `clean`: clean all temporary data (images, containers, volumes, temp data) created by cross.

The initial implementation was done by Marc Schreiber, schrieveslaach.

A few more environment variables exist to fine-tune performance, as well as handle private dependencies.
- `CROSS_REMOTE_COPY_REGISTRY`: copy the cargo registry
- `CROSS_REMOTE_COPY_CACHE`: copy cache directories, including the target directory.

Both of these generally lead to substantial performance penalties, but can enable the use of private SSH dependencies. In either case, the use of persistent data volumes is highly recommended.

Fixes #248.
Fixes #273.
Closes #449.

Co-authored-by: Alex Huszagh <[email protected]>
@Alexhuszagh
Copy link
Contributor Author

bors r-

There's a merge conflict I think, or we might get a bad merge. Going to rebase and merge.

@bors
Copy link
Contributor

bors bot commented Jun 23, 2022

Canceled.

@Alexhuszagh
Copy link
Contributor Author

Nevermind, my fault, it actually merges fine.
bors r=Emilgardis

@bors
Copy link
Contributor

bors bot commented Jun 23, 2022

Build succeeded:

@bors bors bot merged commit 90e44e6 into cross-rs:main Jun 23, 2022
@Alexhuszagh Alexhuszagh deleted the remote branch June 23, 2022 23:26
@Emilgardis Emilgardis added this to the v0.2.2 milestone Jul 1, 2022
@Alexhuszagh Alexhuszagh mentioned this pull request Nov 23, 2022
11 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Recommended GitLab usage Cross fails to mount if docker daemon not running on same machine
3 participants