diff --git a/.github/workflows/move-cli.yml b/.github/workflows/move-cli.yml index 34957502..974a38bb 100644 --- a/.github/workflows/move-cli.yml +++ b/.github/workflows/move-cli.yml @@ -103,3 +103,42 @@ jobs: tag_name: ${{ github.event.inputs.version || github.event.workflow_run.head_branch }} files: initia-move-cli-*.tar.gz + docker-build-push: + needs: [linux-build] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Download Linux AMD64 artifact + uses: actions/download-artifact@v4 + with: + name: x86_64-unknown-linux-gnu-build + path: tools/initia-move-cli + + - name: Extract binary + run: | + cd tools/initia-move-cli + tar -xzvf initia-move-cli-*.tar.gz + rm initia-move-cli-*.tar.gz + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: tools/initia-move-cli + file: tools/initia-move-cli/Dockerfile + push: true + provenance: false + platforms: linux/amd64 + tags: | + ghcr.io/${{ github.repository_owner }}/initia-move-cli:latest + ghcr.io/${{ github.repository_owner }}/initia-move-cli:${{ github.event.inputs.version || github.event.workflow_run.head_branch }} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 7d4816e8..fa82b9ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1421,6 +1421,9 @@ dependencies = [ "anyhow", "clap 4.5.15", "initia-move-compiler", + "movevm", + "serde", + "serde_json", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b96b8ff8..e767036f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ initia-move-gas = { path = "crates/gas" } initia-move-compiler = { path = "crates/compiler" } initia-move-json = { path = "crates/json" } initia-move-resource-viewer = { path = "crates/resource-viewer" } +movevm = { path = "libmovevm" } # External crate dependencies. # Please do not add any test features here: they should be declared by the individual crate. diff --git a/libmovevm/src/lib.rs b/libmovevm/src/lib.rs index c6876c0f..8e815a9e 100644 --- a/libmovevm/src/lib.rs +++ b/libmovevm/src/lib.rs @@ -7,7 +7,7 @@ mod error; mod interface; mod iterator; mod memory; -mod move_api; +pub mod move_api; mod result; mod storage; mod table_storage; diff --git a/libmovevm/src/move_api/handler.rs b/libmovevm/src/move_api/handler.rs index 9b2f40eb..c66ef2d9 100644 --- a/libmovevm/src/move_api/handler.rs +++ b/libmovevm/src/move_api/handler.rs @@ -17,7 +17,7 @@ struct ModuleInfoResponse { pub name: String, } -pub(crate) fn read_module_info(compiled: &[u8]) -> Result, Error> { +pub fn read_module_info(compiled: &[u8]) -> Result, Error> { let m = CompiledModule::deserialize_with_config(compiled, &DeserializerConfig::default()) .map_err(|e| Error::backend_failure(e.to_string()))?; @@ -76,7 +76,7 @@ pub(crate) fn decode_move_value( serde_json::to_vec(&value).map_err(|e| Error::BackendFailure { msg: e.to_string() }) } -pub(crate) fn decode_script_bytes(script_bytes: Vec) -> Result, Error> { +pub fn decode_script_bytes(script_bytes: Vec) -> Result, Error> { let script: MoveScriptBytecode = MoveScriptBytecode::new(script_bytes); let abi = script .try_parse_abi() @@ -86,7 +86,7 @@ pub(crate) fn decode_script_bytes(script_bytes: Vec) -> Result, Erro serde_json::to_vec(&abi).map_err(|e| Error::BackendFailure { msg: e.to_string() }) } -pub(crate) fn decode_module_bytes(module_bytes: Vec) -> Result, Error> { +pub fn decode_module_bytes(module_bytes: Vec) -> Result, Error> { // deserialized request from the json let module: MoveModuleBytecode = MoveModuleBytecode::new(module_bytes); let abi = module diff --git a/tools/initia-move-cli/Cargo.toml b/tools/initia-move-cli/Cargo.toml index 161120ca..9784cb3a 100644 --- a/tools/initia-move-cli/Cargo.toml +++ b/tools/initia-move-cli/Cargo.toml @@ -21,4 +21,7 @@ path = "src/main.rs" [dependencies] anyhow.workspace = true clap.workspace = true -initia-move-compiler.workspace = true \ No newline at end of file +initia-move-compiler.workspace = true +movevm.workspace = true +serde.workspace = true +serde_json.workspace = true diff --git a/tools/initia-move-cli/Dockerfile b/tools/initia-move-cli/Dockerfile new file mode 100644 index 00000000..44cec072 --- /dev/null +++ b/tools/initia-move-cli/Dockerfile @@ -0,0 +1,11 @@ +FROM debian:bullseye-slim + +RUN apt-get update && \ + apt-get install -y --no-install-recommends git ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /usr/local/bin +COPY initia-move-cli . +RUN chmod +x initia-move-cli + +ENTRYPOINT ["initia-move-cli"] \ No newline at end of file diff --git a/tools/initia-move-cli/README.md b/tools/initia-move-cli/README.md index 7b45bdde..0e79bfe8 100644 --- a/tools/initia-move-cli/README.md +++ b/tools/initia-move-cli/README.md @@ -72,38 +72,39 @@ sudo install -m 755 initia-move-cli /usr/local/bin/initia-move rm initia-move-cli-$VERSION-linux-arm64.tar.gz ``` -### How to use it +### Using Docker Container + +You can run the Move CLI tool using Docker without installing it locally. Here's how to use it: ```bash -Initia Move CLI - -Usage: initia-move [OPTIONS] - -Commands: - build Build the package at `path`. If no path is provided defaults to current directory - coverage Inspect test coverage for this package - new Create a new Move package - test Run Move unit tests in this package - help Print this message or the help of the given subcommand(s) - -Options: - -p, --path Path to a package which the command should be run with respect to - -v Print additional diagnostics if available - -d, --dev Compile in 'dev' mode. The 'dev-addresses' and 'dev-dependencies' fields will be used if this flag is set. This flag is - useful for development of packages that expose named addresses that are not set to a specific value - --test Compile in 'test' mode. The 'dev-addresses' and 'dev-dependencies' fields will be used along with any code in the 'tests' - directory - --override-std Whether to override the standard library with the given version [possible values: mainnet, testnet, devnet] - --doc Generate documentation for packages - --abi Generate ABIs for packages - --install-dir Installation directory for compiled artifacts. Defaults to current directory - --force Force recompilation of all packages - --fetch-deps-only Only fetch dependency repos to MOVE_HOME - --skip-fetch-latest-git-deps Skip fetching latest git dependencies - --bytecode-version Bytecode version to compile move code - --skip-attribute-checks Do not complain about an unknown attribute in Move code - --compiler-version Compiler version to use - --language-version Language version to support - --experiments Experiments for v2 compiler to set to true +docker run --rm \ + -v "$(pwd):/code:delegated" \ + -w /code \ + ghcr.io/initia-labs/initia-move-cli:latest \ + +``` +Example commands: +```bash +# Build Move modules +docker run --rm -v "$(pwd):/code" -w /code ghcr.io/initia-labs/initia-move-cli:latest build + +# Run tests +docker run --rm -v "$(pwd):/code" -w /code ghcr.io/initia-labs/initia-move-cli:latest test + +# Decode Move module +docker run --rm -v "$(pwd):/code" -w /code ghcr.io/initia-labs/initia-move-cli:latest decode read my_package my_module ``` + +For easier use, you can create an alias in your shell: +```bash +alias initia-move='docker run --rm -v "$(pwd):/code" -w /code ghcr.io/initia-labs/initia-move-cli:latest' +``` + +Then use it like the native command: +```bash +initia-move build +initia-move test + +### How to use it +initia-move diff --git a/tools/initia-move-cli/src/decode.rs b/tools/initia-move-cli/src/decode.rs new file mode 100644 index 00000000..288ec1b1 --- /dev/null +++ b/tools/initia-move-cli/src/decode.rs @@ -0,0 +1,156 @@ +use crate::{InitiaCLI, InitiaCommand}; +use anyhow::Context; +use clap::{Parser, Subcommand}; +use movevm::move_api::handler::{decode_module_bytes, decode_script_bytes, read_module_info}; +use std::{fs, path::PathBuf}; + +#[derive(Parser)] +#[command( + name = "decode", + about = "Read or Decode Move modules and scripts", + long_about = "Read or Decode Move modules and prints the result in JSON format" +)] +pub struct Decode { + #[command(subcommand)] + pub command: DecodeCommands, +} + +#[derive(Subcommand)] +pub enum DecodeCommands { + #[command( + name = "read", + about = "Read Move module info from bytecode", + long_about = "Read and display basic information about a Move module from its bytecode file.\n\ + Example: initia-move decode read ./build/package/bytecode_modules/my_module.mv" + )] + Read { + #[arg(value_name = "PACKAGE_NAME")] + package_name: String, + #[arg(value_name = "MODULE_NAME")] + module_name: String, + #[clap( + long = "path", + short = 'p', + value_name = "PACKAGE_PATH", + help = "Path to the package directory" + )] + package_path: Option, + }, + + #[command( + name = "script", + about = "Decode Move script bytecode", + long_about = "Decode Move script bytecode and display its ABI (Application Binary Interface).\n\ + Example: initia-move decode script ./build/package/scripts/my_script.mv" + )] + Script { + #[arg(value_name = "PACKAGE_NAME")] + package_name: String, + #[arg(value_name = "SCRIPT_NAME")] + script_name: String, + #[clap( + long = "path", + short = 'p', + value_name = "PACKAGE_PATH", + help = "Path to the package directory" + )] + package_path: Option, + }, + + #[command( + name = "module", + about = "Decode Move module bytecode", + long_about = "Decode Move module bytecode and display its ABI (Application Binary Interface).\n\ + Example: initia-move decode module ./build/package/bytecode_modules/my_module.mv" + )] + Module { + #[arg(value_name = "PACKAGE_NAME")] + package_name: String, + #[arg(value_name = "MODULE_NAME")] + module_name: String, + #[clap( + long = "path", + short = 'p', + value_name = "PACKAGE_PATH", + help = "Path to the package directory" + )] + package_path: Option, + }, +} + +pub trait Decoder { + fn decode(self) -> anyhow::Result<()>; +} + +fn read_file(package_path: &Option, path: &str) -> anyhow::Result> { + let current_dir = package_path + .clone() + .unwrap_or_else(|| std::env::current_dir().expect("Failed to get current directory")); + let file_path = current_dir.join(PathBuf::from(path)); + fs::read(&file_path).with_context(|| format!("Failed to read file: {}", file_path.display())) +} + +impl Decoder for InitiaCLI { + fn decode(self) -> anyhow::Result<()> { + match &self.cmd { + InitiaCommand::Decode(cmd) => { + match &cmd.command { + DecodeCommands::Read { + package_name, + module_name, + package_path, + } => { + let path = + format!("build/{}/bytecode_modules/{}.mv", package_name, module_name); + let bytes = read_file(package_path, &path)?; + let result = read_module_info(&bytes)?; + let mut json: serde_json::Value = serde_json::from_slice(&result)?; + + if let Some(address) = json.get_mut("address") { + if let serde_json::Value::Array(bytes) = address { + let hex = format!( + "0x{}", + bytes + .iter() + .filter_map(|b| b.as_u64()) + .map(|b| format!("{:02x}", b)) + .collect::() + ); + *address = serde_json::json!(hex); + } + } + println!("{}", serde_json::to_string_pretty(&json)?); + } + DecodeCommands::Script { + package_name, + script_name, + package_path, + } => { + let path = format!( + "build/{}/scripts/bytecode_scripts/{}.mv", + package_name, script_name + ); + let bytes = read_file(package_path, &path)?; + let result = decode_script_bytes(bytes)?; + let json: serde_json::Value = serde_json::from_slice(&result)?; + println!("{}", serde_json::to_string_pretty(&json)?); + } + DecodeCommands::Module { + package_name, + module_name, + package_path, + } => { + let path = + format!("build/{}/bytecode_modules/{}.mv", package_name, module_name); + let bytes = read_file(package_path, &path)?; + let result = decode_module_bytes(bytes)?; + let json: serde_json::Value = serde_json::from_slice(&result)?; + println!("{}", serde_json::to_string_pretty(&json)?); + } + } + Ok(()) + } + _ => unreachable!(), + } + } +} diff --git a/tools/initia-move-cli/src/execute.rs b/tools/initia-move-cli/src/execute.rs index 269b7dd4..b0cda18c 100644 --- a/tools/initia-move-cli/src/execute.rs +++ b/tools/initia-move-cli/src/execute.rs @@ -1,4 +1,3 @@ -use anyhow::Error; use initia_move_compiler::{execute, Command}; use crate::{InitiaCLI, InitiaCommand}; @@ -8,13 +7,14 @@ pub trait Execute { } impl Execute for InitiaCLI { - fn execute(self) -> anyhow::Result<(), Error> { + fn execute(self) -> anyhow::Result<()> { let move_args = self.move_args; let cmd = match self.cmd { InitiaCommand::Build(build) => Command::Build(build), InitiaCommand::Coverage(coverage) => Command::Coverage(coverage), InitiaCommand::New(new) => Command::New(new), InitiaCommand::Test(test) => Command::Test(test), + _ => unreachable!(), }; execute(move_args, cmd) } diff --git a/tools/initia-move-cli/src/main.rs b/tools/initia-move-cli/src/main.rs index af1cf61d..ae16939c 100644 --- a/tools/initia-move-cli/src/main.rs +++ b/tools/initia-move-cli/src/main.rs @@ -1,6 +1,8 @@ +mod decode; mod execute; use clap::Parser; +use decode::{Decode, Decoder}; use execute::Execute; use initia_move_compiler::{ base::{build::Build, coverage::Coverage, test::Test}, @@ -12,37 +14,44 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); #[derive(Parser)] pub enum InitiaCommand { /// Build the package at `path`. If no path is provided defaults to current directory + #[command(flatten_help = true)] Build(Build), /// Inspect test coverage for this package + #[command(flatten_help = true)] Coverage(Coverage), /// Create a new Move package + #[command(flatten_help = true)] New(New), /// Run Move unit tests in this package + #[command(flatten_help = true)] Test(Test), + + /// Decode Move modules and scripts + #[command()] + Decode(Decode), } #[derive(Parser)] -#[command( - name = "initia-move", - about = "Initia Move CLI", - version = VERSION -)] +#[command(name = "initia-move", about = "Initia Move CLI", version = VERSION)] pub struct InitiaCLI { - #[clap(flatten)] - pub move_args: Move, - #[clap(subcommand)] pub cmd: InitiaCommand, + + #[command(flatten)] + pub move_args: Move, } fn main() -> anyhow::Result<()> { let cli = InitiaCLI::parse(); - if let Err(e) = cli.execute() { - eprintln!("Error: {}", e); - std::process::exit(1); + match cli.cmd { + InitiaCommand::Decode(_) => cli.decode()?, + InitiaCommand::Build(_) + | InitiaCommand::Coverage(_) + | InitiaCommand::New(_) + | InitiaCommand::Test(_) => cli.execute()?, } Ok(()) }