Skip to content

Conversation

@socksy
Copy link
Contributor

@socksy socksy commented Dec 8, 2025

Proof of concept - WDYT?

Reasoning:

  1. It sucks to have a brittle bunch of uv downloads. I can guarantee you it won't work in NixOS ;)
  2. We are building with cargo as part of the build process
  3. UV is also in Rust, and can be built as part of the built process
  4. We can add UV as a separate target in our Cargo.toml, and then build it accordingly
  5. This lets us be able to delete the download files
  6. Unfortunately, uv does not yet have a stable library interface to use directly in rust, and do not recommend you to do that. And calling the cli main function directly in the code means we would have to dramatically refactor our tokio code somehow
  7. As such, we make a wrapper script that just calls the built code - it'll get installed next to the cli
  8. To actually be able to depend on it, and see it working, add a tower doctor command that checks to see if it can find the uv we downloaded


outputs = { self, nixpkgs, flake-utils, rust-overlay, naersk }:
flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-darwin" "x86_64-darwin" "aarch64-linux" ] (system:
outputs =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

most of the changes in this file are unfortunately from just running nixfmt. Only real changes are in tower-rpm-{ARCH} and tower-deb-{ARCH} where we also copy and chmod uv-wrapper.

@@ -1,2 +1,2 @@
[toolchain]
channel = "1.88"
channel = "1.89"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

uv depends on at least this version of rust

1. It sucks to have a brittle bunch of uv downloads. I can guarantee you
   it won't work in NixOS ;)
2. We are building with cargo as part of the build process
3. UV is also in Rust, and can be built as part of the built process
4. We can add UV as a separate target in our Cargo.toml, and then build
   it accordingly
5. This lets us be able to delete the download files
6. Unfortunately, uv does not yet have a stable library interface to use
   directly in rust, and do not recommend you to do that. And calling
   the cli main function directly in the code means we would have to
   dramatically refactor our tokio code somehow
7. As such, we make a wrapper script that just calls the built code -
   it'll get installed next to the cli
8. To actually be able to depend on it, and see it working, add a
   `tower doctor` command that checks to see if it can find the uv we
   downloaded
@github-actions

This comment was marked as resolved.

@socksy socksy changed the base branch from main to develop December 8, 2025 22:10
@tower tower deleted a comment from github-actions bot Dec 8, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the UV installation mechanism by building UV from source as a separate binary target instead of downloading pre-built binaries for different platforms. This eliminates the brittle platform-specific download logic and improves compatibility, particularly with NixOS. The implementation adds a uv-wrapper binary alongside the main tower CLI and includes a new tower doctor command to verify the installation.

Key changes:

  • Replaced UV download logic with a build-from-source approach using a uv-wrapper binary
  • Added tower doctor command to validate UV availability and configuration
  • Updated Rust toolchain from 1.88 to 1.89

Reviewed changes

Copilot reviewed 10 out of 12 changed files in this pull request and generated no comments.

Show a summary per file
File Description
rust-toolchain.toml Updated Rust channel version to support new dependencies
flake.nix Added uv-wrapper binary to package distributions and reformatted Nix code
crates/tower-uv/tests/install_test.rs Replaced UV download test with wrapper existence verification
crates/tower-uv/src/lib.rs Removed download logic and implemented wrapper discovery mechanism
crates/tower-uv/src/install.rs Deleted entire platform-specific download implementation
crates/tower-uv/Cargo.toml Added uv dependency and defined uv-wrapper binary target
crates/tower-cmd/src/lib.rs Integrated doctor command into CLI
crates/tower-cmd/src/doctor.rs New diagnostic command implementation
crates/tower-cmd/Cargo.toml Added tower-uv dependency for doctor command
Cargo.toml Updated minimum Rust version to 1.89

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@bradhe
Copy link
Contributor

bradhe commented Dec 10, 2025

I thought about this over night. I think the premise for the problem here is your first bullet point: It sucks to download uv on demand. Why exactly does it suck? We're relying on someone else's infrastructure to work correctly, we don't get strong guarantees about what we download (unless we start packaging a SHA with this), probably a few other reasons? How important are they?

Packaging uv with tower is a nice optimization, it lets us make better guarantees about what content will be on the host using tower. Why do we need to build it though? Why not just download it--or vendor all the versions for all the platforms we support?

@socksy
Copy link
Contributor Author

socksy commented Dec 24, 2025

Hmm, looks like I didn't post a reply, even though I was sure I had. I think this is what I said:

I think the premise for the problem here is your first bullet point: It sucks to download uv on demand. Why exactly does it suck? We're relying on someone else's infrastructure to work correctly, we don't get strong guarantees about what we download (unless we start packaging a SHA with this), probably a few other reasons? How important are they?

I think downloading executable code at runtime is a worry and a codesmell, and I think trying to divine what code is going to run on the machine we're currently running on is a brittle process that's likely to break. I know for a fact it doesn't work on NixOS, which isn't a huge concern usually (people truly using it tend to have workarounds involving distrobox to run ubuntu etc for when a software isn't compliant), but is usually a sign to me that something's a bit wrong.

I'm not too worried about relying on other people's infra — sure, it sucks to have to rely on infra to be up. But we rely on that same infra to be up when you install the software, and it's not like we do this every time on running, just the first time, so I don't think colocating the binaries improves resilience here much.

Packaging uv with tower is a nice optimization, it lets us make better guarantees about what content will be on the host using tower. Why do we need to build it though? Why not just download it--or vendor all the versions for all the platforms we support?

For me it's just the simplest and easiest solution, since it by definitions means that you can guarantee that the binary for tower and the binary for uv will be compatible — since they were built for the same architecture, at the same time, with the same process. And it's semantically the same as saying "this package depends upon this one being built", which is how we'd do it if it were a normal rust lib, so it feels like relying on cargo to deal with all that stuff for us is ideal. But I'm definitely not married to the idea.

RE: the other options

  • vendoring (which afaik isn't really done much in rust land?) would mean tracking large binaries in the repo
  • fetching on build would mean you still have the brittleness/duplicated logic of matching architectures/OSes to current build.

But I think both of those are still preferable to downloading at runtime, so would be happy if we explore those too. As seen by the failing tests in the CI here, it clearly isn't a walk in the park to build a simultaneous binary with uv/maturin and expect it to always be where you want it, and all the solutions (e.g. the tests are always in a subdir, so let's always check the parent dir for the binary as well as the current one) seem like janky workarounds to me.

IMO ideally we'd depend upon UV as a lib in cargo — since it is a dependency, and it is in rust, and it makes a lot of sense to me to then let cargo just do its thing. But as explored before, they both discourage that use case and also we would have to rearchitect how we spawn processes for runs, since we can't use the existing tokio code we have for that, except if we wrap their main CLI entry point, and spawn that... which is exactly what this PR does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants