diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84928f357c9..9f7bba2a8eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,6 +78,25 @@ jobs: command: run args: --bin build-all-examples + build-mdbook: + needs: cancel-previous-runs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run mdbook build + uses: peaceiris/actions-mdbook@v1 + with: + mdbook-version: '0.4.7' + - name: Emit logs to tmp.txt, fail if build logs contain 'ERROR' + run: | + mdbook build docs &> tmp.txt + if cat tmp.txt | grep 'ERROR' + then + rm tmp.txt && exit 1 + else + rm tmp.txt && exit 0 + fi + cargo-build-workspace: needs: cancel-previous-runs runs-on: ubuntu-latest @@ -206,6 +225,27 @@ jobs: with: command: test + forc-documenter-dry-run: + needs: cancel-previous-runs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + - uses: Swatinem/rust-cache@v1 + - name: Install Forc + uses: actions-rs/cargo@v1 + with: + command: install + args: --debug --path ./forc + - name: Run forc-documenter in dry run mode + uses: actions-rs/cargo@v1 + with: + command: run + args: --bin forc-documenter write-docs --dry-run + cargo-unused-deps-check: needs: cancel-previous-runs runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index fe20c0d78ff..7b57a6e0e18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1093,6 +1093,14 @@ dependencies = [ "whoami", ] +[[package]] +name = "forc-documenter" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 3.1.6", +] + [[package]] name = "forc-explore" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 3c31bbc1e6f..abe50235e62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "sway-utils", "test", "test-sig-gen-util", + "scripts/forc-documenter" ] exclude = [ "examples/fizzbuzz", diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 4e0988f6e79..ed8f0be1457 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -32,6 +32,20 @@ - [Identifiers](./blockchain-development/identifiers.md) - [Native Assets](./blockchain-development/native_assets.md) - [Calling Contracts](./blockchain-development/calling_contracts.md) +- [Forc](./forc/index.md) + - [Commands](./forc/commands/index.md) + - [forc addr2line](./forc/commands/forc_addr2line.md) + - [forc build](./forc/commands/forc_build.md) + - [forc clean](./forc/commands/forc_clean.md) + - [forc completions](./forc/commands/forc_completions.md) + - [forc deploy](./forc/commands/forc_deploy.md) + - [forc init](./forc/commands/forc_init.md) + - [forc json-abi](./forc/commands/forc_json-abi.md) + - [forc parse-bytecode](./forc/commands/forc_parse-bytecode.md) + - [forc plugins](./forc/commands/forc_plugins.md) + - [forc run](./forc/commands/forc_run.md) + - [forc test](./forc/commands/forc_test.md) + - [forc update](./forc/commands/forc_update.md) - [Testing](./testing/index.md) - [Testing with Rust](./testing/testing-with-rust.md) - [Advanced Concepts](./advanced/index.md) diff --git a/docs/src/forc/commands/forc_addr2line.md b/docs/src/forc/commands/forc_addr2line.md new file mode 100644 index 00000000000..f54ae736f89 --- /dev/null +++ b/docs/src/forc/commands/forc_addr2line.md @@ -0,0 +1,29 @@ +# forc-addr2line +Show location and context of an opcode address in its source file + + +## USAGE: +forc addr2line [OPTIONS] --sourcemap-path --opcode-index + + +## OPTIONS: + +`-c`, `--context` <_CONTEXT_> + +How many lines of context to show [default: 2] + +`-g`, `--sourcemap-path` <_SOURCEMAP_PATH_> + +Source file mapping in JSON format + +`-h`, `--help` + +Print help information + +`-i`, `--opcode-index` <_OPCODE_INDEX_> + +Opcode index + +`-s`, `--search-dir` <_SEARCH_DIR_> + +Where to search for the project root [default: .] \ No newline at end of file diff --git a/docs/src/forc/commands/forc_build.md b/docs/src/forc/commands/forc_build.md new file mode 100644 index 00000000000..6e916324733 --- /dev/null +++ b/docs/src/forc/commands/forc_build.md @@ -0,0 +1,104 @@ +# forc-build +Compile the current or target project. + +The output produced will depend on the project's program type. Building script, predicate and +contract projects will produce their bytecode in binary format `.bin`. Building +contracts and libraries will also produce the public ABI in JSON format `-abi.json`. + + +## USAGE: +forc build [OPTIONS] + + +## OPTIONS: + +`-g`, `--debug-outfile` <_DEBUG_OUTFILE_> + + +If set, outputs source file mapping in JSON format + + +`-h`, `--help` + + +Print help information + + +`--minify-json-abi` + + +By default the JSON for ABIs is formatted for human readability. By using this option +JSON output will be "minified", i.e. all on one line without whitespace + + +`-o` <_BINARY_OUTFILE_> + + +If set, outputs a binary file representing the script bytes + + +`--offline` + + +Offline mode, prevents Forc from using the network when managing dependencies. Meaning +it will only try to use previously downloaded dependencies + + +`--output-directory` <_OUTPUT_DIRECTORY_> + + +The directory in which the sway compiler output artifacts are placed. + +By default, this is `/out`. + + +`-p`, `--path` <_PATH_> + + +Path to the project, if not specified, current working directory will be used + + +`--print-finalized-asm` + + +Whether to compile to bytecode (false) or to print out the generated ASM (true) + + +`--print-intermediate-asm` + + +Whether to compile to bytecode (false) or to print out the generated ASM (true) + + +`--print-ir` + + +Whether to compile to bytecode (false) or to print out the generated IR (true) + + +`-s`, `--silent` + + +Silent mode. Don't output any warnings or errors to the command line + + +`--use-orig-asm` + + +Whether to compile using the original (pre- IR) pipeline + +## EXAMPLES: + +Compile the sway files of the current project. + +```console +$ forc build +Compiled script "my-fuel-project". +Bytecode size is 28 bytes. +``` + +The output produced will depend on the project's program type. Building script, predicate and contract projects will produce their bytecode in binary format `.bin`. Building contracts and libraries will also produce the public ABI in JSON format `-abi.json`. + +By default, these artifacts are placed in the `out/` directory. + +If a `Forc.lock` file did not yet exist, it will be created in order to pin each of the dependencies listed in `Forc.toml` to a specific commit or version. \ No newline at end of file diff --git a/docs/src/forc/commands/forc_clean.md b/docs/src/forc/commands/forc_clean.md new file mode 100644 index 00000000000..7f55d5439dd --- /dev/null +++ b/docs/src/forc/commands/forc_clean.md @@ -0,0 +1,19 @@ +# forc-clean +Removes the default forc compiler output artifact directory, i.e. `/out`. Also calls +`cargo clean` which removes the `target` directory generated by `cargo` when running tests + + +## USAGE: +forc clean [OPTIONS] + + +## OPTIONS: + +`-h`, `--help` + +Print help information + +`-p`, `--path` <_PATH_> + +Path to the project, if not specified, current working directory will be +used \ No newline at end of file diff --git a/docs/src/forc/commands/forc_completions.md b/docs/src/forc/commands/forc_completions.md new file mode 100644 index 00000000000..0845dc8a044 --- /dev/null +++ b/docs/src/forc/commands/forc_completions.md @@ -0,0 +1,125 @@ +# forc-completions +Generate tab-completion scripts for your shell + + +## USAGE: +forc completions --shell + + +## OPTIONS: + +`-h`, `--help` + +Print help information + +`-s`, `--shell` <_SHELL_> + +[possible values: zsh, bash, fish, powershell, elvish] + + +DISCUSSION: +Enable tab completion for Bash, Fish, Zsh, or PowerShell +The script is output on `stdout`, allowing one to re-direct the +output to the file of their choosing. Where you place the file +will depend on which shell, and which operating system you are +using. Your particular configuration may also determine where +these scripts need to be placed. + +Here are some common set ups for the three supported shells under +Unix and similar operating systems (such as GNU/Linux). + +BASH: + +Completion files are commonly stored in `/etc/bash_completion.d/` for +system-wide commands, but can be stored in +`~/.local/share/bash-completion/completions` for user-specific commands. +Run the command: + +$ mkdir -p ~/.local/share/bash-completion/completions +$ forc completions --shell=bash >> ~/.local/share/bash-completion/completions/forc + +This installs the completion script. You may have to log out and +log back in to your shell session for the changes to take effect. + +BASH (macOS/Homebrew): + +Homebrew stores bash completion files within the Homebrew directory. +With the `bash-completion` brew formula installed, run the command: + +$ mkdir -p $(brew --prefix)/etc/bash_completion.d +$ forc completions --shell=bash > $(brew --prefix)/etc/bash_completion.d/forc.bash- +completion + +FISH: + +Fish completion files are commonly stored in +`$HOME/.config/fish/completions`. Run the command: + +$ mkdir -p ~/.config/fish/completions +$ forc completions --shell=fish > ~/.config/fish/completions/forc.fish + +This installs the completion script. You may have to log out and +log back in to your shell session for the changes to take effect. + +ZSH: + +ZSH completions are commonly stored in any directory listed in +your `$fpath` variable. To use these completions, you must either +add the generated script to one of those directories, or add your +own to this list. + +Adding a custom directory is often the safest bet if you are +unsure of which directory to use. First create the directory; for +this example we'll create a hidden directory inside our `$HOME` +directory: + +$ mkdir ~/.zfunc + +Then add the following lines to your `.zshrc` just before +`compinit`: + +fpath+=~/.zfunc + +Now you can install the completions script using the following +command: + +$ forc completions --shell=zsh > ~/.zfunc/_forc + +You must then either log out and log back in, or simply run + +$ exec zsh + +for the new completions to take effect. + +CUSTOM LOCATIONS: + +Alternatively, you could save these files to the place of your +choosing, such as a custom directory inside your $HOME. Doing so +will require you to add the proper directives, such as `source`ing +inside your login script. Consult your shells documentation for +how to add such directives. + +POWERSHELL: + +The powershell completion scripts require PowerShell v5.0+ (which +comes with Windows 10, but can be downloaded separately for windows 7 +or 8.1). + +First, check if a profile has already been set + +PS C:\> Test-Path $profile + +If the above command returns `False` run the following + +PS C:\> New-Item -path $profile -type file -force + +Now open the file provided by `$profile` (if you used the +`New-Item` command it will be +`${env:USERPROFILE}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1` + +Next, we either save the completions file into our profile, or +into a separate file and source it inside our profile. To save the +completions into our profile simply use + +PS C:\> forc completions --shell=powershell >> +${env:USERPROFILE}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 \ No newline at end of file diff --git a/docs/src/forc/commands/forc_deploy.md b/docs/src/forc/commands/forc_deploy.md new file mode 100644 index 00000000000..dca482b3d00 --- /dev/null +++ b/docs/src/forc/commands/forc_deploy.md @@ -0,0 +1,102 @@ +# forc-deploy +Deploy contract project. Crafts a contract deployment transaction then sends it to a running node + + +## USAGE: +forc deploy [OPTIONS] + + +## OPTIONS: + +`-g`, `--debug-outfile` <_DEBUG_OUTFILE_> + + +If set, outputs source file mapping in JSON format + + +`-h`, `--help` + + +Print help information + + +`--minify-json-abi` + + +By default the JSON for ABIs is formatted for human readability. By using this option +JSON output will be "minified", i.e. all on one line without whitespace + + +`-o` <_BINARY_OUTFILE_> + + +If set, outputs a binary file representing the script bytes + + +`--offline` + + +Offline mode, prevents Forc from using the network when managing dependencies. Meaning +it will only try to use previously downloaded dependencies + + +`--output-directory` <_OUTPUT_DIRECTORY_> + + +The directory in which the sway compiler output artifacts are placed. + +By default, this is `/out`. + + +`-p`, `--path` <_PATH_> + + +Path to the project, if not specified, current working directory will be used + + +`--print-finalized-asm` + + +Whether to compile to bytecode (false) or to print out the generated ASM (true) + + +`--print-intermediate-asm` + + +Whether to compile to bytecode (false) or to print out the generated ASM (true) + + +`--print-ir` + + +Whether to compile to bytecode (false) or to print out the IR (true) + + +`-s`, `--silent` + + +Silent mode. Don't output any warnings or errors to the command line + + +`--use-orig-asm` + + +Whether to compile using the original (pre- IR) pipeline + +## EXAMPLES: + +Deploy contract project. Crafts a contract deployment transaction then sends it to a running node. + +Alternatively, you could deploy your contract programmatically using our SDK: + +```rust +// Build the contract +let salt: [u8; 32] = rng.gen(); +let salt = Salt::from(salt); +let compiled = Contract::compile_sway_contract("./", salt).unwrap(); + +// Launch a local network and deploy the contract +let compiled = Contract::compile_sway_contract("./", salt).unwrap(); +let client = Provider::launch(Config::local_node()).await.unwrap(); +let contract_id = Contract::deploy(&compiled, &client).await.unwrap(); +``` \ No newline at end of file diff --git a/docs/src/forc/commands/forc_init.md b/docs/src/forc/commands/forc_init.md new file mode 100644 index 00000000000..0fc01c83e8c --- /dev/null +++ b/docs/src/forc/commands/forc_init.md @@ -0,0 +1,48 @@ +# forc-init +Create a new Forc project + + +## USAGE: +forc init [OPTIONS] + + +## ARGS: + +<_PROJECT_NAME_> + + The name of your project + + +## OPTIONS: + +`-h`, `--help` + +Print help information + +`-t`, `--template` <_TEMPLATE_> + +Initialize a new project from a template. + +Example Templates: +- counter + +## EXAMPLES: + +```console +$ forc init my-fuel-project +$ cd my-fuel-project +$ tree +. +├── Cargo.toml +├── Forc.toml +├── src +│ └── main.sw +└── tests + └── harness.rs +``` + +`Forc.toml` is the Forc manifest file, containing information about the project and dependencies. `Cargo.toml` is the Rust project manifest file, used by the Rust-based tests package. + +A `src/` directory is created, with a single `main.sw` Sway file in it. + +A `tests/` directory is also created. The `Cargo.toml` in the root directory contains necessary Rust dependencies to enable you to write Rust-based tests using our Rust SDK (`fuels-rs`). More on this in the `Test` section down below. \ No newline at end of file diff --git a/docs/src/forc/commands/forc_json-abi.md b/docs/src/forc/commands/forc_json-abi.md new file mode 100644 index 00000000000..550e782ce0f --- /dev/null +++ b/docs/src/forc/commands/forc_json-abi.md @@ -0,0 +1,38 @@ +# forc-json-abi +Output the JSON associated with the ABI + + +## USAGE: +forc json-abi [OPTIONS] + + +## OPTIONS: + +`-h`, `--help` + +Print help information + +`--minify` + +By default the JSON for ABIs is formatted for human readability. By +using this option JSON output will be "minified", i.e. all on one line +without whitespace + +`-o` <_JSON_OUTFILE_> + +If set, outputs a json file representing the output json abi + +`--offline` + +Offline mode, prevents Forc from using the network when managing +dependencies. Meaning it will only try to use previously downloaded +dependencies + +`-p`, `--path` <_PATH_> + +Path to the project, if not specified, current working directory will +be used + +`-s`, `--silent` + +Silent mode. Don't output any warnings or errors to the command line \ No newline at end of file diff --git a/docs/src/forc/commands/forc_parse-bytecode.md b/docs/src/forc/commands/forc_parse-bytecode.md new file mode 100644 index 00000000000..50eebe8130f --- /dev/null +++ b/docs/src/forc/commands/forc_parse-bytecode.md @@ -0,0 +1,41 @@ +# forc-parse-bytecode +Parse bytecode file into a debug format + + +## USAGE: +forc parse-bytecode + + +## ARGS: + +<_FILE_PATH_> + + +## OPTIONS: + +`-h`, `--help` + +Print help information + +## EXAMPLES: + +Example with the initial project created using `forc init`: + +```console +$ forc build -o obj +Compiled script "my-fuel-project". +Bytecode size is 28 bytes. +``` + +```console +my-second-project$ forc parse-bytecode obj + + half-word byte op raw notes + 0 0 JI(4) [144, 0, 0, 4] conditionally jumps to byte 16 + 1 4 NOOP [71, 0, 0, 0] + 2 8 Undefined [0, 0, 0, 0] data section offset lo (0) + 3 12 Undefined [0, 0, 0, 28] data section offset hi (28) + 4 16 LW(46, 12, 1) [93, 184, 192, 1] + 5 20 ADD(46, 46, 12) [16, 186, 227, 0] + 6 24 RET(0) [36, 0, 0, 0] +``` \ No newline at end of file diff --git a/docs/src/forc/commands/forc_plugins.md b/docs/src/forc/commands/forc_plugins.md new file mode 100644 index 00000000000..3567d4bb701 --- /dev/null +++ b/docs/src/forc/commands/forc_plugins.md @@ -0,0 +1,16 @@ +# forc-plugins +Find all forc plugins available via `PATH`. + +Prints the absolute path to each discovered plugin on a new line. + + +## USAGE: +forc plugins + + +## OPTIONS: + +`-h`, `--help` + + +Print help information \ No newline at end of file diff --git a/docs/src/forc/commands/forc_run.md b/docs/src/forc/commands/forc_run.md new file mode 100644 index 00000000000..b3c1ddeda9f --- /dev/null +++ b/docs/src/forc/commands/forc_run.md @@ -0,0 +1,135 @@ +# forc-run +Run script project. Crafts a script transaction then sends it to a running node + + +## USAGE: +forc run [OPTIONS] [NODE_URL] + + +## ARGS: + +<_NODE_URL_> +URL of the Fuel Client Node + +[env: FUEL_NODE_URL=] +[default: 127.0.0.1:4000] + + +## OPTIONS: + +`--byte-price` <_BYTE_PRICE_> + + +Set the transaction byte price. Defaults to 0 + + +`--contract` <_CONTRACT_> + + +32-byte contract ID that will be called during the transaction + + +`-d`, `--data` <_DATA_> + + +Hex string of data to input to script + + +`--dry-run` + + +Only craft transaction and print it out + + +`-g`, `--debug-outfile` <_DEBUG_OUTFILE_> + + +If set, outputs source file mapping in JSON format + + +`--gas-limit` <_GAS_LIMIT_> + + +Set the transaction gas limit. Defaults to the maximum gas limit + + +`--gas-price` <_GAS_PRICE_> + + +Set the transaction gas price. Defaults to 0 + + +`-h`, `--help` + + +Print help information + + +`-k`, `--kill-node` + + +Kill Fuel Node Client after running the code. This is only available if the node is +started from `forc run` + + +`--minify-json-abi` + + +By default the JSON for ABIs is formatted for human readability. By using this option +JSON output will be "minified", i.e. all on one line without whitespace + + +`-o` <_BINARY_OUTFILE_> + + +If set, outputs a binary file representing the script bytes + + +`--output-directory` <_OUTPUT_DIRECTORY_> + + +The directory in which the sway compiler output artifacts are placed. + +By default, this is `/out`. + + +`-p`, `--path` <_PATH_> + + +Path to the project, if not specified, current working directory will be used + + +`--print-finalized-asm` + + +Whether to compile to bytecode (false) or to print out the generated ASM (true) + + +`--print-intermediate-asm` + + +Whether to compile to bytecode (false) or to print out the generated ASM (true) + + +`--print-ir` + + +Whether to compile to bytecode (false) or to print out the IR (true) + + +`-r`, `--pretty-print` + + +Pretty-print the outputs from the node + + +`-s`, `--silent` + + +Silent mode. Don't output any warnings or errors to the command line + + +`--use-orig-asm` + + +Whether to compile using the original (pre- IR) pipeline \ No newline at end of file diff --git a/docs/src/forc/commands/forc_test.md b/docs/src/forc/commands/forc_test.md new file mode 100644 index 00000000000..d328a9420f5 --- /dev/null +++ b/docs/src/forc/commands/forc_test.md @@ -0,0 +1,56 @@ +# forc-test +Run Rust-based tests on current project. As of now, `forc test` is a simple wrapper on `cargo test`; +`forc init` also creates a rust package under your project, named `tests`. You can opt to either run +these Rust tests by using `forc test` or going inside the package and using `cargo test` + + +## USAGE: +forc test [OPTIONS] [TEST_NAME] [-- ...] + + +## ARGS: + +<_TEST_NAME_> +If specified, only run tests containing this string in their names + + +<_CARGO_TEST_ARGS_> + +.. +All trailing arguments following `--` are collected within this argument. + +E.g. Given the following: + +`forc test -- foo bar baz` + +The arguments `foo`, `bar` and `baz` are forwarded on to `cargo test` like so: + +`cargo test -- foo bar baz` + + +## OPTIONS: + +`--cargo-test-opts` <_CARGO_TEST_OPTS_> + + +Options passed through to the `cargo test` invocation. + +E.g. Given the following: + +`forc test --cargo-test-opts="--color always"` + +The `--color always` option is forwarded to `cargo test` like so: + +`cargo test --color always` + + +`-h`, `--help` + + +Print help information + +## EXAMPLES: + +You can write tests in Rust using our [Rust SDK](https://github.com/FuelLabs/fuels-rs). These tests can be run using `forc test`, which will look for Rust tests under the `tests/` directory (which is created automatically with `forc init`). + +You can find an example under the [Testing with Rust](../../testing/testing-with-rust.md) section. \ No newline at end of file diff --git a/docs/src/forc/commands/forc_update.md b/docs/src/forc/commands/forc_update.md new file mode 100644 index 00000000000..bcea6766891 --- /dev/null +++ b/docs/src/forc/commands/forc_update.md @@ -0,0 +1,29 @@ +# forc-update +Update dependencies in the Forc dependencies directory + + +## USAGE: +forc update [OPTIONS] + + +## OPTIONS: + +`-c`, `--check` + +Checks if the dependencies have newer versions. Won't actually +perform the update, will output which ones are up-to-date and +outdated + +`-d` <_TARGET_DEPENDENCY_> + +Dependency to be updated. If not set, all dependencies will be +updated + +`-h`, `--help` + +Print help information + +`-p`, `--path` <_PATH_> + +Path to the project, if not specified, current working directory +will be used \ No newline at end of file diff --git a/docs/src/forc/commands/index.md b/docs/src/forc/commands/index.md new file mode 100644 index 00000000000..2158899a4b4 --- /dev/null +++ b/docs/src/forc/commands/index.md @@ -0,0 +1,14 @@ +Here are a list of commands available to forc: + +- [forc addr2line](./forc_addr2line.md) +- [forc build](./forc_build.md) +- [forc clean](./forc_clean.md) +- [forc completions](./forc_completions.md) +- [forc deploy](./forc_deploy.md) +- [forc init](./forc_init.md) +- [forc json-abi](./forc_json-abi.md) +- [forc parse-bytecode](./forc_parse-bytecode.md) +- [forc plugins](./forc_plugins.md) +- [forc run](./forc_run.md) +- [forc test](./forc_test.md) +- [forc update](./forc_update.md) diff --git a/docs/src/forc/index.md b/docs/src/forc/index.md new file mode 100644 index 00000000000..f307e378d19 --- /dev/null +++ b/docs/src/forc/index.md @@ -0,0 +1,5 @@ +# Forc + +Forc stands for Fuel Orchestrator. Forc provides a variety of tools and commands for developers working with the Fuel ecosystem, such as scaffolding a new project, formatting, running scripts, deploying contracts, testing contracts, and more. If you're coming from a Rust background, forc is similar to cargo. + +- [Commands](./commands/index.md) diff --git a/docs/src/testing/testing-with-rust.md b/docs/src/testing/testing-with-rust.md index fb59d66664c..28d73e39ceb 100644 --- a/docs/src/testing/testing-with-rust.md +++ b/docs/src/testing/testing-with-rust.md @@ -27,8 +27,56 @@ For example, let's write tests against the following contract, written in Sway. Our `tests/harness.rs` file could look like: + ```rust,ignore -{{#include ../../../examples/hello_world/tests/harness.rs}} +use fuel_tx::Salt; +use fuels_abigen_macro::abigen; +use fuels_contract::contract::Contract; +use fuels_contract::parameters::TxParameters; +use fuels_signers::util::test_helpers::setup_test_provider_and_wallet; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; + +// Generate Rust bindings from our contract JSON ABI +abigen!(MyContract, "./out/debug/hello_world-abi.json"); + +#[tokio::test] +async fn harness() { + let rng = &mut StdRng::seed_from_u64(2322u64); + + // Build the contract + let salt: [u8; 32] = rng.gen(); + let salt = Salt::from(salt); + + // Launch a local network and deploy the contract + let compiled = Contract::load_sway_contract("./out/debug/hello_world.bin", salt).unwrap(); + let (provider, wallet) = setup_test_provider_and_wallet().await; + let contract_id = Contract::deploy(&compiled, &provider, &wallet, TxParameters::default()) + .await + .unwrap(); + println!("Contract deployed @ {:x}", contract_id); + + let contract_instance = MyContract::new(contract_id.to_string(), provider, wallet); + + // Call `initialize_counter()` method in our deployed contract. + // Note that, here, you get type-safety for free! + let result = contract_instance + .initialize_counter(42) + .call() + .await + .unwrap(); + + assert_eq!(42, result.value); + + // Call `increment_counter()` method in our deployed contract. + let result = contract_instance + .increment_counter(10) + .call() + .await + .unwrap(); + + assert_eq!(52, result.value); +} ``` Then, in the root of our project, running `forc test` will run the test above, compiling and deploying the contract to a local Fuel network, and calling the ABI methods against the contract deployed in there: diff --git a/scripts/forc-documenter/Cargo.toml b/scripts/forc-documenter/Cargo.toml new file mode 100644 index 00000000000..79b8c678f37 --- /dev/null +++ b/scripts/forc-documenter/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "forc-documenter" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.41" +clap = { version = "3.1", features = ["env", "derive"] } \ No newline at end of file diff --git a/scripts/forc-documenter/README.md b/scripts/forc-documenter/README.md new file mode 100644 index 00000000000..410b409867b --- /dev/null +++ b/scripts/forc-documenter/README.md @@ -0,0 +1,67 @@ +# Forc Documenter + +Forc documenter is a script to update the Forc commands section of the Sway book based on the output we get when running `forc --help`. It does the following 3 things: + +1. Create a new page in the Sway book for a forc command, showing descriptions, usage, options and examples (if any) +2. Updates the index.md for the commands section to list all the available commands +3. Updates SUMMARY.md to reflect the updates in the commands + +## Prerequisites + +Since the script uses `forc`, make sure you have the latest Forc version installed: + +```rust +// install the latest version of forc +cargo install forc +``` + +## Usage + +You can run the script in `--dry-run` mode for it to tell you if there were changes within commands that were not updated, without writing any files: + +```rust +cargo run --bin forc-documenter write-docs --dry-run +``` + +In the case of an inconsistency, you will receive a prompt to rebuild docs: + +```console +cargo run --bin forc-documenter write-docs --dry-run + Finished dev [unoptimized + debuginfo] target(s) in 0.24s + Running `target/debug/forc-documenter write-docs --dry-run` +forc addr2line: documentation ok. +Error: Documentation inconsistent for forc build - please run `cargo run --bin forc-documenter write-docs` +``` + +The above is the same command that runs within CI to ensure docs are updated. + +You can use the script without options to update the docs and write the affected files: + +```rust +cargo run --bin forc-documenter write-docs +``` + +This is the output you will see, should the script run successfully: + +```console +cargo run --bin forc-documenter write-docs + Finished dev [unoptimized + debuginfo] target(s) in 0.25s + Running `target/debug/forc-documenter write-docs` +Generating docs for command: forc addr2line... +Generating docs for command: forc build... +Generating docs for command: forc clean... +Generating docs for command: forc completions... +Generating docs for command: forc deploy... +Generating docs for command: forc explorer... +Generating docs for command: forc fmt... +Generating docs for command: forc init... +Generating docs for command: forc json-abi... +Generating docs for command: forc lsp... +Generating docs for command: forc parse-bytecode... +Generating docs for command: forc run... +Generating docs for command: forc test... +Generating docs for command: forc update... +Updating forc commands in forc/commands/index.md... +Updating forc commands in SUMMARY.md... +Done. +``` diff --git a/scripts/forc-documenter/src/checkers.rs b/scripts/forc-documenter/src/checkers.rs new file mode 100644 index 00000000000..faf881a0a88 --- /dev/null +++ b/scripts/forc-documenter/src/checkers.rs @@ -0,0 +1,67 @@ +use crate::constants; +use anyhow::{anyhow, Result}; +use std::fs::File; +use std::io::Read; + +pub fn is_option(token: &str) -> bool { + token.starts_with('-') +} + +pub fn is_arg(token: &str) -> bool { + token.starts_with('<') +} + +pub fn is_args_line(line: &str) -> bool { + line.trim().starts_with('<') +} + +pub fn is_options_line(line: &str) -> bool { + line.trim().starts_with('-') && line.trim().chars().nth(1).unwrap() != ' ' +} + +pub fn check_summary_diffs( + existing_summary_contents: String, + new_summary_contents: String, +) -> Result<()> { + if existing_summary_contents == new_summary_contents { + println!("SUMMARY.md ok."); + } else { + return Err(anyhow!( + "SUMMARY.md inconsistent - {}", + constants::RUN_WRITE_DOCS_MESSAGE + )); + } + + Ok(()) +} + +pub fn check_index_diffs(mut index_file: File, new_index_contents: String) -> Result<()> { + let mut existing_index_contents = String::new(); + index_file.read_to_string(&mut existing_index_contents)?; + if existing_index_contents == new_index_contents { + println!("index.md ok."); + } else { + return Err(anyhow!( + "index.md inconsistent - {}", + constants::RUN_WRITE_DOCS_MESSAGE + )); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_options_line() { + let example_option_line_1= " -s, --silent Silent mode. Don't output any warnings or errors to the command line"; + let example_option_line_2 = " -o If set, outputs a json file representing the output json abi"; + let example_option_line_3 = " - counter"; + + assert!(is_options_line(example_option_line_1)); + assert!(is_options_line(example_option_line_2)); + assert!(!is_options_line(example_option_line_3)); + } +} diff --git a/scripts/forc-documenter/src/constants.rs b/scripts/forc-documenter/src/constants.rs new file mode 100644 index 00000000000..b11c04fdd86 --- /dev/null +++ b/scripts/forc-documenter/src/constants.rs @@ -0,0 +1,89 @@ +pub const SUBHEADERS: &[&str] = &["USAGE:", "ARGS:", "OPTIONS:", "SUBCOMMANDS:"]; +pub const INDEX_HEADER: &str = "Here are a list of commands available to forc:\n\n"; + +pub static RUN_WRITE_DOCS_MESSAGE: &str = "please run `cargo run --bin forc-documenter write-docs`"; + +pub static EXAMPLES_HEADER: &str = "\n## EXAMPLES:\n"; +pub static FORC_INIT_EXAMPLE: &str = r#" +```console +$ forc init my-fuel-project +$ cd my-fuel-project +$ tree +. +├── Cargo.toml +├── Forc.toml +├── src +│ └── main.sw +└── tests + └── harness.rs +``` + +`Forc.toml` is the Forc manifest file, containing information about the project and dependencies. `Cargo.toml` is the Rust project manifest file, used by the Rust-based tests package. + +A `src/` directory is created, with a single `main.sw` Sway file in it. + +A `tests/` directory is also created. The `Cargo.toml` in the root directory contains necessary Rust dependencies to enable you to write Rust-based tests using our Rust SDK (`fuels-rs`). More on this in the `Test` section down below. +"#; + +pub static FORC_BUILD_EXAMPLE: &str = r#" +Compile the sway files of the current project. + +```console +$ forc build +Compiled script "my-fuel-project". +Bytecode size is 28 bytes. +``` + +The output produced will depend on the project's program type. Building script, predicate and contract projects will produce their bytecode in binary format `.bin`. Building contracts and libraries will also produce the public ABI in JSON format `-abi.json`. + +By default, these artifacts are placed in the `out/` directory. + +If a `Forc.lock` file did not yet exist, it will be created in order to pin each of the dependencies listed in `Forc.toml` to a specific commit or version. +"#; + +pub static FORC_TEST_EXAMPLE: &str = r#" +You can write tests in Rust using our [Rust SDK](https://github.com/FuelLabs/fuels-rs). These tests can be run using `forc test`, which will look for Rust tests under the `tests/` directory (which is created automatically with `forc init`). + +You can find an example under the [Testing with Rust](../../testing/testing-with-rust.md) section. +"#; + +pub static FORC_DEPLOY_EXAMPLE: &str = r#" +Deploy contract project. Crafts a contract deployment transaction then sends it to a running node. + +Alternatively, you could deploy your contract programmatically using our SDK: + +```rust +// Build the contract +let salt: [u8; 32] = rng.gen(); +let salt = Salt::from(salt); +let compiled = Contract::compile_sway_contract("./", salt).unwrap(); + +// Launch a local network and deploy the contract +let compiled = Contract::compile_sway_contract("./", salt).unwrap(); +let client = Provider::launch(Config::local_node()).await.unwrap(); +let contract_id = Contract::deploy(&compiled, &client).await.unwrap(); +``` +"#; + +pub static FORC_PARSE_BYTECODE_EXAMPLE: &str = r#" +Example with the initial project created using `forc init`: + +```console +$ forc build -o obj +Compiled script "my-fuel-project". +Bytecode size is 28 bytes. +``` + +```console +my-second-project$ forc parse-bytecode obj + + half-word byte op raw notes + 0 0 JI(4) [144, 0, 0, 4] conditionally jumps to byte 16 + 1 4 NOOP [71, 0, 0, 0] + 2 8 Undefined [0, 0, 0, 0] data section offset lo (0) + 3 12 Undefined [0, 0, 0, 28] data section offset hi (28) + 4 16 LW(46, 12, 1) [93, 184, 192, 1] + 5 20 ADD(46, 46, 12) [16, 186, 227, 0] + 6 24 RET(0) [36, 0, 0, 0] +``` +"#; diff --git a/scripts/forc-documenter/src/helpers.rs b/scripts/forc-documenter/src/helpers.rs new file mode 100644 index 00000000000..93270bd5537 --- /dev/null +++ b/scripts/forc-documenter/src/helpers.rs @@ -0,0 +1,197 @@ +use crate::checkers::{is_arg, is_args_line, is_option, is_options_line}; +use crate::constants; + +#[derive(PartialEq)] +pub enum LineKind { + SubHeader, + Arg, + Option, + Text, +} + +fn get_line_kind(line: &str) -> LineKind { + if constants::SUBHEADERS.contains(&line) { + LineKind::SubHeader + } else if is_args_line(line) { + LineKind::Arg + } else if is_options_line(line) { + LineKind::Option + } else { + LineKind::Text + } +} +pub fn format_command_doc_name(command: &str) -> String { + "forc_".to_owned() + command + ".md" +} + +pub fn format_index_entry_name(command: &str) -> String { + "forc ".to_owned() + command +} +pub fn format_index_entry_string(document_name: &str, index_entry_name: &str) -> String { + "- [".to_owned() + index_entry_name + "](./" + document_name + ")\n" +} + +pub fn format_line(line: &str) -> String { + match get_line_kind(line) { + LineKind::SubHeader => format_subheader_line(line), + LineKind::Option => format_option_line(line), + LineKind::Arg => format_arg_line(line), + LineKind::Text => line.to_string(), + } +} + +pub fn format_header_line(header_line: &str) -> String { + "\n# ".to_owned() + header_line + "\n" +} + +fn format_subheader_line(subheader_line: &str) -> String { + "\n## ".to_owned() + subheader_line + "\n" +} + +fn format_arg(arg: &str) -> String { + let mut result = String::new(); + let mut inner = arg.to_string(); + + inner.pop(); + inner.remove(0); + + result.push('<'); + result.push('_'); + result.push_str(&inner); + result.push('_'); + result.push('>'); + + result +} + +fn format_arg_line(arg_line: &str) -> String { + let mut formatted_arg_line = String::new(); + + for c in arg_line.chars() { + if c == '>' { + formatted_arg_line.push('_'); + formatted_arg_line.push(c); + } else if c == '<' { + formatted_arg_line.push(c); + formatted_arg_line.push('_'); + } else { + formatted_arg_line.push(c); + } + } + if !formatted_arg_line.trim().ends_with('>') { + let last_closing_bracket_idx = formatted_arg_line.rfind('>').unwrap(); + formatted_arg_line.replace_range( + last_closing_bracket_idx + 1..last_closing_bracket_idx + 2, + "\n\n", + ); + } + "\n".to_owned() + &formatted_arg_line +} + +fn format_option_line(option_line: &str) -> String { + let mut tokens_iter = option_line.trim().split(' '); + + let mut result = String::new(); + let mut rest_of_line = String::new(); + + while let Some(token) = tokens_iter.next() { + if is_option(token) { + result.push_str(&format_option(token)); + } else if is_arg(token) { + result.push_str(&format_arg(token)); + } else { + rest_of_line.push_str(token); + rest_of_line.push(' '); + rest_of_line = tokens_iter + .fold(rest_of_line, |mut a, b| { + a.reserve(b.len() + 1); + a.push_str(b); + a.push(' '); + a + }) + .trim() + .to_owned(); + break; + } + } + result.push_str("\n\n"); + result.push_str(&rest_of_line); + result.push('\n'); + + "\n".to_owned() + &result +} + +fn format_option(option: &str) -> String { + match option.ends_with(',') { + true => { + let mut s = option.to_string(); + s.pop(); + "`".to_owned() + &s + "`, " + } + false => "`".to_owned() + option + "` ", + } +} + +pub fn format_index_line_for_summary(index_line: &str) -> String { + let mut formatted_index_line = String::new(); + let mut pushed = false; + for c in index_line.chars() { + formatted_index_line.push(c); + if c == '.' && !pushed { + pushed = true; + formatted_index_line.push_str("/forc/commands"); + } + } + + formatted_index_line +} + +#[cfg(test)] +mod tests { + use super::{format_arg_line, format_header_line, format_option_line, format_subheader_line}; + + #[test] + fn test_format_header_line() { + let example_header = "forc-fmt"; + let expected_header = "\n# forc-fmt\n"; + + assert_eq!(expected_header, format_header_line(example_header)); + } + + #[test] + fn test_format_subheader_line() { + let example_subheader = "USAGE:"; + let expected_subheader = "\n## USAGE:\n"; + + assert_eq!(expected_subheader, format_subheader_line(example_subheader)); + } + + #[test] + fn test_format_arg_line() { + let example_arg_line_1 = " Some description"; + let example_arg_line_2 = " Some description"; + let expected_arg_line_1 = "\n<_PROJECT_NAME_>\n\nSome description"; + let expected_arg_line_2 = "\n<_arg1_> <_arg2_>\n\nSome description"; + + assert_eq!(expected_arg_line_1, format_arg_line(example_arg_line_1)); + assert_eq!(expected_arg_line_2, format_arg_line(example_arg_line_2)); + } + + #[test] + fn test_format_option_line() { + let example_option_line_1 = "-c, --check Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits with 1 and prints a diff if formatting is required"; + let example_option_line_2 = + "-o If set, outputs a json file representing the output json abi"; + let expected_option_line_1= "\n`-c`, `--check` \n\nRun in 'check' mode. Exits with 0 if input is formatted correctly. Exits with 1 and prints a diff if formatting is required\n"; + let expected_option_line_2 = "\n`-o` <_JSON_OUTFILE_>\n\nIf set, outputs a json file representing the output json abi\n"; + + assert_eq!( + expected_option_line_1, + format_option_line(example_option_line_1) + ); + assert_eq!( + expected_option_line_2, + format_option_line(example_option_line_2) + ); + } +} diff --git a/scripts/forc-documenter/src/main.rs b/scripts/forc-documenter/src/main.rs new file mode 100644 index 00000000000..f217ec5124e --- /dev/null +++ b/scripts/forc-documenter/src/main.rs @@ -0,0 +1,268 @@ +use anyhow::{anyhow, Result}; +use clap::{Parser, Subcommand}; +use std::fs::{create_dir_all, read_to_string, remove_dir_all, File, OpenOptions}; +use std::io::Read; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process; +use std::str; + +mod checkers; +mod constants; +mod helpers; + +use crate::checkers::{check_index_diffs, check_summary_diffs}; +use crate::helpers::{ + format_command_doc_name, format_header_line, format_index_entry_name, + format_index_entry_string, format_index_line_for_summary, format_line, +}; + +#[derive(Parser)] +#[clap(name = "forc-documenter", about = "Forc Documenter")] +struct Cli { + /// the command to run + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + WriteDocs(WriteDocsCommand), +} + +#[derive(Debug, Parser)] +struct WriteDocsCommand { + #[clap(long)] + pub dry_run: bool, +} + +fn get_sway_path() -> PathBuf { + let mut curr_path = std::env::current_dir().unwrap(); + loop { + if curr_path.ends_with("sway") { + return curr_path; + } + curr_path = curr_path.parent().unwrap().to_path_buf() + } +} + +fn create_forc_commands_docs_dir(path: &Path) -> Result<()> { + if !path.is_dir() { + create_dir_all(&path)?; + } + + Ok(()) +} + +fn get_example_for_command(command: &str) -> &str { + match command { + "init" => constants::FORC_INIT_EXAMPLE, + "build" => constants::FORC_BUILD_EXAMPLE, + "test" => constants::FORC_TEST_EXAMPLE, + "deploy" => constants::FORC_DEPLOY_EXAMPLE, + "parse-bytecode" => constants::FORC_PARSE_BYTECODE_EXAMPLE, + _ => "", + } +} + +fn write_new_summary_contents( + existing_summary_contents: String, + new_index_contents: String, +) -> String { + let mut new_summary_contents = String::new(); + for line in existing_summary_contents.lines() { + if line.contains("[Commands](./forc/commands/index.md)") { + new_summary_contents.push_str(line); + new_summary_contents.push('\n'); + for index_line in new_index_contents.lines().skip(2) { + let summary_index_line = format_index_line_for_summary(index_line); + new_summary_contents.push_str(&(" ".to_owned() + &summary_index_line)); + new_summary_contents.push('\n'); + } + } else if line.contains("/forc/commands/") { + continue; + } else { + new_summary_contents.push_str(line); + new_summary_contents.push('\n'); + } + } + new_summary_contents +} + +fn write_docs(command: WriteDocsCommand) -> Result<()> { + let WriteDocsCommand { dry_run } = command; + + let forc_commands_docs_path = get_sway_path().join("docs/src/forc/commands"); + let summary_file_path = get_sway_path().join("docs/src/SUMMARY.md"); + let index_file_path = forc_commands_docs_path.join("index.md"); + + if !dry_run { + remove_dir_all(&forc_commands_docs_path).expect("Failed to clean commands directory"); + create_forc_commands_docs_dir(&forc_commands_docs_path) + .expect("Failed to prepare forc commands docs directory"); + } + let mut index_file = OpenOptions::new() + .create(!dry_run) + .read(true) + .write(!dry_run) + .open(index_file_path) + .expect("Problem reading, opening or creating forc/commands/index.md"); + + let mut summary_file = + File::open(&summary_file_path).expect("Problem reading, opening or creating SUMMARY.md"); + + let mut existing_summary_contents = String::new(); + summary_file.read_to_string(&mut existing_summary_contents)?; + + let version = process::Command::new("forc") + .arg("--version") + .output() + .expect("Failed running forc --version") + .stdout; + + let version_message = + "Running forc --help using ".to_owned() + &String::from_utf8_lossy(&version); + println!("{}", version_message); + + let output = process::Command::new("forc") + .arg("--help") + .output() + .expect("Failed to run help command"); + + let s = String::from_utf8_lossy(&output.stdout); + let lines = s.lines(); + + let mut subcommand_is_parsed = false; + let mut possible_commands = vec![]; + + for line in lines { + if subcommand_is_parsed { + let (command, _) = line.trim().split_once(' ').unwrap_or(("", "")); + possible_commands.push(command); + } + if line == "SUBCOMMANDS:" { + subcommand_is_parsed = true; + } + } + + let mut new_index_contents = String::new(); + new_index_contents.push_str(constants::INDEX_HEADER); + + for command in possible_commands.iter() { + let mut result = match generate_doc_output(command) { + Ok(output) => output, + Err(_) => continue, + }; + + let example = get_example_for_command(command); + if !example.is_empty() { + result.push_str(constants::EXAMPLES_HEADER); + result.push_str(example); + } + result = result.trim().to_string(); + + let document_name = format_command_doc_name(command); + let index_entry_name = format_index_entry_name(command); + let index_entry_string = format_index_entry_string(&document_name, &index_entry_name); + + let forc_command_file_path = forc_commands_docs_path.join(document_name); + new_index_contents.push_str(&index_entry_string); + + if dry_run { + let existing_contents = read_to_string(&forc_command_file_path); + match existing_contents { + Ok(existing_contents) => { + if existing_contents == result { + println!("forc {}: documentation ok.", &command); + } else { + return Err(anyhow!( + "Documentation inconsistent for forc {} - {}", + &command, + constants::RUN_WRITE_DOCS_MESSAGE + )); + } + } + Err(_) => { + return Err(anyhow!( + "Documentation does not exist for forc {} - {}", + &command, + constants::RUN_WRITE_DOCS_MESSAGE + )); + } + } + } else { + println!("Generating docs for command: forc {}...", &command); + let mut command_file = + File::create(&forc_command_file_path).expect("Failed to create documentation"); + command_file + .write_all(result.as_bytes()) + .expect("Failed to write to file"); + } + } + + let new_summary_contents = write_new_summary_contents( + existing_summary_contents.clone(), + new_index_contents.clone(), + ); + if dry_run { + check_index_diffs(index_file, new_index_contents)?; + check_summary_diffs(existing_summary_contents, new_summary_contents)?; + } else { + println!("Updating forc commands in forc/commands/index.md..."); + index_file + .write_all(new_index_contents.as_bytes()) + .expect("Failed to write to forc/commands/index.md"); + + let mut new_summary_file = File::create(&summary_file_path)?; + println!("Updating forc commands in SUMMARY.md..."); + new_summary_file + .write_all(new_summary_contents.as_bytes()) + .expect("Failed to write to SUMMARY.md"); + } + + println!("Done."); + Ok(()) +} + +fn generate_doc_output(subcommand: &str) -> Result { + let mut result = String::new(); + + let output = process::Command::new("forc") + .args([subcommand, "--help"]) + .output() + .expect("forc --help failed to run"); + + if !output.status.success() { + return Err(anyhow!("Failed to run forc {} --help", subcommand)); + } + + let s = String::from_utf8_lossy(&output.stdout); + + for (index, line) in s.lines().enumerate() { + let mut formatted_line = String::new(); + let line = line.trim(); + + if index == 0 { + formatted_line.push_str(&format_header_line(line)); + } else if index == 1 { + formatted_line.push_str(line); + } else { + formatted_line.push_str(&format_line(line)) + } + + result.push_str(&formatted_line); + + if !formatted_line.ends_with('\n') { + result.push('\n'); + } + } + Ok(result) +} +fn main() -> Result<()> { + let cli = Cli::parse(); + + match cli.command { + Commands::WriteDocs(command) => write_docs(command)?, + } + Ok(()) +}