Skip to content

Commit

Permalink
feat(cli, config): --func, --check, single contract compilation (#287)
Browse files Browse the repository at this point in the history
* switch from `args` to the `meow` CLI framework

* `tact.config.json` also defines flags to stop compilation after typechecking or generating FunC code

* CLI flags override the ones in the `tact.config.json`

Co-authored-by: Novus Nota <68142933+novusnota@users.noreply.github.com>
  • Loading branch information
vitorpy and novusnota authored May 2, 2024
1 parent 6aa2512 commit bc086fb
Show file tree
Hide file tree
Showing 13 changed files with 311 additions and 66 deletions.
35 changes: 30 additions & 5 deletions .github/workflows/tact.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ jobs:
cache: "yarn"

- name: Install dependencies
run: yarn install

run: |
corepack enable
yarn install
- name: Build and Test Tact compiler
run: |
Expand All @@ -54,17 +57,39 @@ jobs:
run: |
type examples\output\echo_Echo.pkg
- name: Compare Tact version from CLI flag `--version` against package.json
- name: Install the package locally
run: |
npm install -g ./
- name: CLI Test: Compare Tact version from CLI flag `--version` against package.json
if: runner.os != 'Windows'
run: |
if [ "$(./bin/tact --version)" != "$(jq -r '.version' < package.json)" ];
if [ "$(tact --version)" != "$(jq -r '.version' < package.json)" ];
then false
fi
- name: Tact CLI non-zero exit code
- name: CLI Test: Check single-contract compilation
run: |
tact --check test/tact-cli/success.tact
tact --func test/tact-cli/success.tact
tact test/tact-cli/success.tact
- name: CLI Test: Check compilation via `--config`
run: |
# should output complete results
tact --config test/tact-cli/success.config.json
# should only run the syntax and type checking
tact --config test/tact-cli/success.config.json --check
- name: CLI Test: Check parsing of a non-existing CLI flag
if: runner.os != 'Windows'
run: |
! tact --nonexistentoption test/tact-cli/success.config.json
- name: CLI Test: tact executable return non-zero exit code if compilation fails
if: runner.os != 'Windows'
run: |
! ./bin/tact --config test/tact-cli/tact.config.json
! tact --config test/tact-cli/fail.config.json
- name: Link Tact compiler
run: |
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The `pow2` power function with base 2: PR [#267](https://github.com/tact-lang/tact/pull/267)
- The `try` and `try-catch` statements: PR [#212](https://github.com/tact-lang/tact/pull/212)
- The `del` method for the `Map` type: PR [#95](https://github.com/tact-lang/tact/pull/95)
- The `-h`/`--help`, `-v` (short for `--version`), `-p` (short for `--project`), `--func` (for only outputting FunC code) and `--check` (for only doing the syntax and type checking) command-line flags: PR [#287](https://github.com/tact-lang/tact/pull/287)
- The `mode` enum in project properties of `tact.config.json` for specifying compilation mode: `full` (default), `funcOnly` (only outputs FunC code and exits), or `checkOnly` (only does the syntax and type checking, then exits): PR [#287](https://github.com/tact-lang/tact/pull/287)

### Changed

Expand All @@ -34,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use `|` instead of `+` for send mode flags because the bitwise OR operation is idempotent and hence safer: PR [#274](https://github.com/tact-lang/tact/pull/274)
- Bumped the versions of `@ton/core` and `ohm-js` to the most recent ones: PR [#276](https://github.com/tact-lang/tact/pull/276)
- Generated `.pkg`-files always use POSIX file paths (even on Windows): PR [# 300](https://github.com/tact-lang/tact/pull/300)
- The `-p`/`--project` flags now allow specifying more than one project name. Additionally, they also require a `--config` flag to be specified: PR [#287](https://github.com/tact-lang/tact/pull/287)
- Command-line interface now allows compiling a single Tact file directly, without specifying a config: PR [#287](https://github.com/tact-lang/tact/pull/287)

### Fixed

Expand Down
178 changes: 142 additions & 36 deletions bin/tact
Original file line number Diff line number Diff line change
@@ -1,38 +1,144 @@
#!/usr/bin/env node
const main = require("../dist/node");
const arg = require("arg");

// Resolve arguments
const args = arg({
"--config": String,
"--project": String,
"--version": Boolean,
});

if (args["--version"]) {
console.log("1.2.0");
return;
}

if (!args["--config"]) {
console.log("USAGE: tact --config <config_path> [--project <project_name]");
return;
}

// Perform compilation
(async () => {
try {
const success = await main.run({
configPath: args["--config"],
projectNames: args["--project"] ? args["--project"] : [],
});
// https://nodejs.org/docs/v20.12.1/api/process.html#exit-codes
if (!success) process.exit(30);
} catch (e) {
console.warn(
"Internal compiler error. Please, report it to https://github.com/tact-lang/tact/issues.",

// @ts-nocheck
const pkg = require("../package.json");
const main = require("../dist/node.js");
const meowModule = import("meow");

meowModule.then(
/** @param meow {import('meow/build/index')} */
(meow) => {
const cli = meow.default(
`
Usage
$ tact [...flags] (--config CONFIG | FILE)
Flags
-c, --config CONFIG Specify path to config file (tact.config.json)
-p, --project ...names Build only the specified project name(s) from the config file
--func Output intermediate FunC code and exit
--check Perform syntax and type checking, then exit
-v, --version Print Tact compiler version and exit
-h, --help Display this text and exit
Examples
$ tact --version
${pkg.version}
Learn more about Tact: https://docs.tact-lang.org
Join Telegram group: https://t.me/tactlang
Follow X/Twitter account: https://twitter.com/tact_language`,
{
importMeta: {
url: new URL("file://" + __dirname + __filename).toString(),
},
description: `Command-line utility for the Tact compiler:\n${pkg.description}`,
flags: {
config: {
shortFlag: "c",
type: "string",
isRequired: (flags, _) => {
// Require a config when the projects are specified AND version/help are not specified
if (
flags.projects.length !== 0 &&
!flags.version &&
!flags.help
) {
return true;
}
// Don't require it otherwise
return false;
},
},
projects: { shortFlag: "p", type: "string", isMultiple: true },
func: { type: "boolean", default: false },
check: { type: "boolean", default: false },
version: { shortFlag: "v", type: "boolean" },
help: { shortFlag: "h", type: "boolean" },
},
allowUnknownFlags: false,
},
);
console.log(e);
process.exit(30);
}
})();

// Helper function to write less in following checks
const isEmptyConfigAndInput = () => {
if (cli.flags.config === undefined && cli.input.length === 0) {
return true;
}
return false;
};

// Show help regardless of other flags
if (cli.flags.help) {
cli.showHelp(0);
}

// Show version regardless of other flags
if (cli.flags.version) {
cli.showVersion();
}

// Disallow specifying both config or Tact source file at the same time
if (cli.flags.config !== undefined && cli.input.length > 0) {
console.log(
"Error: Both config and Tact file can't be simultaneously specified, pick one!",
);
cli.showHelp();
}

// Disallow specifying both --func and --check flags at the same time
if (cli.flags.check && cli.flags.func) {
console.log("Error: Flags --func and --check are mutually exclusive!");
cli.showHelp();
}

// Disallow running --func and --check flags without a config or a file specified
if (isEmptyConfigAndInput() && (cli.flags.check || cli.flags.func)) {
console.log("Error: Either config or Tact file have to be specified!");
cli.showHelp();
}

// Disallow specifying more than one Tact file
if (cli.input.length > 1) {
console.log(
"Error: Only one Tact file can be specified at a time. If you want more, provide a config!",
);
cli.showHelp();
}

// Show help when all flags and inputs are empty
// Note, that version/help flags are already processed above and don't need to be mentioned here
if (
isEmptyConfigAndInput() &&
!cli.flags.check &&
!cli.flags.func &&
cli.flags.projects.length === 0
) {
cli.showHelp(0);
}

// Compilation mode
const mode = cli.flags.check
? "checkOnly"
: cli.flags.func
? "funcOnly"
: undefined;

// TODO: all flags on the cli should take precendence over flags in the config
// Make a nice model for it in the src/node.ts instead of the current mess
// Consider making overwrites right here or something.

// Main command
main
.run({
fileName: cli.input.at(0),
configPath: cli.flags.config,
projectNames: cli.flags.projects ?? [],
additionalCliOptions: { mode },
})
.then((success) => {
// https://nodejs.org/docs/v20.12.1/api/process.html#exit-codes
process.exit(success ? 0 : 30);
});
},
);
11 changes: 11 additions & 0 deletions grammar/configSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,17 @@
"properties": {
"debug": {
"type": "boolean",
"default": false,
"description": "False by default. If set to true, enables debug output of a contract and allows usage of `dump()` function, which is useful for debugging purposes. With this option enabled, the contract will report that it was compiled in debug mode using the supported_interfaces method.\n\nRead more on debugging Tact code: https://docs.tact-lang.org/book/debug."
},
"masterchain": {
"type": "boolean",
"default": false,
"description": "False by default. If set to true, enables masterchain support.\n\nRead more about masterchain: https://docs.tact-lang.org/book/masterchain."
},
"external": {
"type": "boolean",
"default": false,
"description": "False by default. If set to true, enables support of external message receivers.\n\nRead more about external message receivers: https://docs.tact-lang.org/book/external."
},
"experimental": {
Expand All @@ -47,11 +50,19 @@
"properties": {
"inline": {
"type": "boolean",
"default": false,
"description": "False by default. If set to true, enables inlining of all functions in contracts. This can reduce gas usage at the cost of bigger contracts."
}
}
}
}
},
"mode": {
"type": "string",
"default": "full",
"enum": ["full", "checkOnly", "funcOnly"],
"title": "Compilation mode of the project.",
"description": "Set to `full` by default, which runs the whole pipeline of the compilation and emits FunC code, BoC and various utility files, including wrappers for TypeScript.\nIf set to `checkOnly`, only performs syntax and type checking, preventing further compilation.\nIf set to `funcOnly`, only outputs intermediate FunC code, preventing further compilation."
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@
"@tact-lang/opcode": "^0.0.14",
"@ton/core": "0.56.3",
"@ton/crypto": "^3.2.0",
"arg": "^5.0.2",
"blockstore-core": "1.0.5",
"change-case": "^4.1.2",
"ipfs-unixfs-importer": "9.0.10",
"meow": "^13.2.0",
"mkdirp": "^2.1.3",
"multiformats": "^13.1.0",
"ohm-js": "^17.1.0",
Expand Down
1 change: 1 addition & 0 deletions src/config/parseConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const projectSchema = z
path: z.string(),
output: z.string(),
options: optionsSchema.optional(),
mode: z.enum(["full", "checkOnly", "funcOnly"]).optional(),
})
.strict();

Expand Down
Loading

0 comments on commit bc086fb

Please sign in to comment.