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

[DRAFT FOR COMMENTS] move: compile deps with prior compiler binary versions #15245

Closed
wants to merge 1 commit into from

Conversation

rvantonder
Copy link
Contributor

@rvantonder rvantonder commented Dec 7, 2023

Description

We want to enable compiling packages with dependencies that specify prior compiler versions in their Move.lock file (with respective edition and flavor flags). The main thing this does is allow to check byte-for-byte equality of a package build / its dependents against published on-chain code.

Example of the info we store in Move.lock (implemented in #15166):

        [move.toolchain-version]
        compiler-version = "1.16.0"
        edition = "legacy"
        flavor = "sui"

This PR implements an end-to-end package build that will pick up prior compiler-versions for dependencies, download a prior Sui binary, compile them, and "add" them to the build. The (most viable?) approach that seems to enable that is adding deps to the logic in external-crates/move/crates/move-package/src/compilation/compiled_package.rs after compiling with prior versions.

This is the main question for reviewers: does this approach check out? Does this approach violate internal assumptions or impact other build / applications that I may not be aware of?

See inline comments for more.

Test Plan

How did you test the new or updated feature?


If your changes are not user-facing and not a breaking change, you can skip the following section. Otherwise, please indicate what changed, and then add to the Release Notes section as highlighted during the release process.

Type of Change (Check all that apply)

  • protocol change
  • user-visible impact
  • breaking change for a client SDKs
  • breaking change for FNs (FN binary must upgrade)
  • breaking change for validators or node operators (must upgrade binaries)
  • breaking change for on-chain data layout
  • necessitate either a data wipe or data migration

Release notes

Copy link

vercel bot commented Dec 7, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
mysten-ui ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 7, 2023 5:42am
sui-core ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 7, 2023 5:42am
sui-typescript-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 7, 2023 5:42am
3 Ignored Deployments
Name Status Preview Comments Updated (UTC)
explorer ⬜️ Ignored (Inspect) Visit Preview Dec 7, 2023 5:42am
multisig-toolkit ⬜️ Ignored (Inspect) Visit Preview Dec 7, 2023 5:42am
sui-kiosk ⬜️ Ignored (Inspect) Visit Preview Dec 7, 2023 5:42am

Comment on lines +461 to +469
let (deps_for_current_compiler, deps_for_prior_compiler) =
partition_deps_by_toolchain(deps_package_paths.clone())?;

let flags = resolution_graph.build_options.compiler_flags();
// invoke the compiler
let mut paths = deps_package_paths.clone();
let mut paths = deps_for_current_compiler;
paths.push(sources_package_paths.clone());

let compiler = Compiler::from_package_paths(paths, vec![])
let compiler = Compiler::from_package_paths(paths, deps_for_prior_compiler)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Request for input: this is the "main" change. In effect, we compile deps whose Move.lock specify a prior compiler version other than the one we're on. For those deps, we pass their package paths in the second arg to from_package_paths, which is one route I found that appears to work well and completes the build.

One change in the build directory output I observe though, is that, prior to this change, the build directory would include dependency .mv files in build/dependencies. With this change, .mv files of dependencies compiled with a prior compiler do not end up in build/dependencies (and I'm not sure they need to?).

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to test this change? That is, add a new Move project that uses Move 2024, but relies on a Move Legacy version of code that is already published? This would allow use to ensure the change supports on-chain verification while building correctly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, I think moving the .mv files to build/dependencies/ is a good and worthwhile goal if we can. it will allow us to cache dependencies by-version, etc., in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it possible to test this change? That is, add a new Move project that uses Move 2024, but relies on a Move Legacy version of code that is already published? This would allow use to ensure the change supports on-chain verification while building correctly.

I tested locally to get a successful build, and will check on-chain verification with a proper PR. This PR was meant to get a sense of how to do the build, which I think is ~mostly clear to me now.

Also, I think moving the .mv files to build/dependencies/ is a good and worthwhile goal if we can. it will allow us to cache dependencies by-version, etc., in the future.

Right, so this is the kind of thing that I was a bit unsure about re how the internals work when build/dependencies exist. I.e., if we copy / move .mv to build/dependencies would caching "just work"?

let dest_binary_os = OsStr::new(dest_binary.as_path());

if !dest_binary.exists() {
// Download if binary does not exist.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm just using curl/tar to test this end-to-end, it can and will probably be replaced with native Rust reqwest, etc.

OsStr::new(root.as_path()),
])
.output()
.expect("failed to build package");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

.expect just a shortcut for end-to-end testing, will format proper errors after comments

Comment on lines +781 to +785
if !lock_file.exists() {
// Q: Behavior to pick when package has no lock file. Right now we choose the current compiler (we could use the "legacy" one instead).
deps_for_current_compiler.push(dep);
continue;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implementation detail: which compiler version makes sense to pick when there is no lock file (which may be the case for existing packages). My intuition is we go with the latest / current one.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that makes sense? Though I don't fully understand under what situations we don't have a lock file

Copy link
Contributor

Choose a reason for hiding this comment

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

I think any time we don't have a lock file, we should generate one? And if that is the case, yes, we should use the latest.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, we generate lock files if they don't exist.

situations we don't have a lock file

People might not check it in on a repo

Comment on lines +790 to +793
None => {
// Q: Behavior to pick when package has no [move.toolchain-version] info. Right now we choose the current compiler (we could use the "legacy" one instead).
deps_for_current_compiler.push(dep)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implementation detail: ditto for when there's no toolchain version info.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we fix the version for what version of the lock file added this feature? (which I assume is this one?)

Comment on lines +781 to +785
if !lock_file.exists() {
// Q: Behavior to pick when package has no lock file. Right now we choose the current compiler (we could use the "legacy" one instead).
deps_for_current_compiler.push(dep);
continue;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that makes sense? Though I don't fully understand under what situations we don't have a lock file

Comment on lines +790 to +793
None => {
// Q: Behavior to pick when package has no [move.toolchain-version] info. Right now we choose the current compiler (we could use the "legacy" one instead).
deps_for_current_compiler.push(dep)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we fix the version for what version of the lock file added this feature? (which I assume is this one?)

}
Some(ToolchainVersion {
compiler_version, ..
}) if compiler_version == env!("CARGO_PKG_VERSION") => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to parse or format these at all?
I suppose it is not critical, but longterm we might need to if the format changes here.

Comment on lines +801 to +803
// Compile and mark that we compiled this dep with a prior compiler.
download_and_compile(root, toolchain_version)?;
deps_for_prior_compiler.push(dep)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we make this a map? Like collect all deps for version X and compile them all at once.
Or is it not worth it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Echoing this suggestion: assemble each dep by what version it wants, as Map<Version, Vec<Dep>> or similar. While it likely isn't necessary for this logic to work, it may allow future extensions (blacklisting versions, allowing users to filter versions, etc.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Noted! I think it makes sense to keep a Map that groups by version, but for V0 of the implementation, in the interest of speed, will probably leave "compile all at once by version" on the table for a second iteration.

}
Some(ToolchainVersion {
compiler_version, ..
}) if compiler_version == env!("CARGO_PKG_VERSION") => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit, bind this constant somewhere

flavor,
}: ToolchainVersion,
) -> Result<()> {
let binaries_path = std::env::var("TOOLCHAIN_BINARIES")?; // E.g., ~/.move/binaries
Copy link
Contributor

Choose a reason for hiding this comment

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

nit bind this constant somewhere


if !dest_binary.exists() {
// Download if binary does not exist.
let url = format!("https://github.com/MystenLabs/sui/releases/download/mainnet-v{}/sui-mainnet-v{}-{}.tgz", compiler_version, compiler_version, platform);
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably extract out this and the path information somewhere

}

println!(
"[+] sui move build --default-move-edition {} --default-move-flavor {} -p {}",
Copy link
Contributor

Choose a reason for hiding this comment

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

I worry about these flags here just in case we accidentally break them. I mean... we shouldn't but we might. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

IMO flag shifting is a real possibility. Perhaps a file that defines invocations per-version?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'll need (a different?) way to build if we can't rely on flags--I'm not aware of another way right now if we're invoking the binaries? I'd love if we can have a high degree of confidence about flags, at least for medium to long term (6+ months?). That way, if they do change, one way to approach this is to have version detection logic added in new releases, that case out and use appropriate flags. Basically, build it into this logic.

Perhaps a file that defines invocations per-version?

Proposal for what this would look like? (Will this mean another file users need to check in, and do we really want that?)

@@ -756,3 +765,123 @@ pub(crate) fn make_source_and_deps_for_compiler(
};
Ok((source_package_paths, deps_package_paths))
}

/// partitions `deps` by whether we need to compile dependent packages with a
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you explain if we handle this case?

  • A requires B at version 0.1.0
  • A requires C at version 0.2.0
  • B requires C at version 0.1.0

If not, I do think we need a map of versions to modules to compile, so any individual module can find its dependencies at the right version.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Based on how we currently resolve dependencies, I'm not sure we can / want to / need to support this case.

If packages in the dependency tree require C at different versions (more plainly, if different C dependencies are specified in Move.tomls based on package name) we detect a conflict. So my understanding based on how we do resolution (which is not entirely complete :P) is that if C exists in the dependency graph it can only be one version.

Copy link
Contributor

@cgswords cgswords left a comment

Choose a reason for hiding this comment

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

This seems like a good initial draft, modulo some concerns about dependency compilation in the limit and generality.

I'd really like to see an actual test on the diff.

Copy link
Contributor Author

@rvantonder rvantonder left a comment

Choose a reason for hiding this comment

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

Thanks for the comments @tnowacki @cgswords! I think I have enough of a sense to go ahead with a PR for merging, with proper tests, etc. so will do that. Feel free to tack on more thoughts in the comments though!

@rvantonder rvantonder closed this Jan 17, 2024
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