tlaplus-cli 0.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tla/__init__.py +0 -0
- tla/build_tlc_module.py +70 -0
- tla/check_java.py +82 -0
- tla/cli.py +70 -0
- tla/config.py +63 -0
- tla/download_tla2tools.py +109 -0
- tla/py.typed +0 -0
- tla/resources/default_config.yaml +23 -0
- tla/run_tlc.py +45 -0
- tla/settings.py +48 -0
- tlaplus_cli-0.1.7.dist-info/METADATA +142 -0
- tlaplus_cli-0.1.7.dist-info/RECORD +16 -0
- tlaplus_cli-0.1.7.dist-info/WHEEL +5 -0
- tlaplus_cli-0.1.7.dist-info/entry_points.txt +2 -0
- tlaplus_cli-0.1.7.dist-info/licenses/LICENSE +21 -0
- tlaplus_cli-0.1.7.dist-info/top_level.txt +1 -0
tla/__init__.py
ADDED
|
File without changes
|
tla/build_tlc_module.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Compile custom TLC modules (Java)."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from tla.config import cache_dir, load_config, workspace_root
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build(
|
|
11
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show compilation output."),
|
|
12
|
+
) -> None:
|
|
13
|
+
"""Compile custom Java modules."""
|
|
14
|
+
config = load_config()
|
|
15
|
+
|
|
16
|
+
# Jar lives in the cache directory
|
|
17
|
+
jar_path = cache_dir() / config.tla.jar_name
|
|
18
|
+
|
|
19
|
+
ws_root = workspace_root()
|
|
20
|
+
modules_dir = ws_root / config.workspace.modules_dir
|
|
21
|
+
classes_dir = ws_root / config.workspace.classes_dir
|
|
22
|
+
|
|
23
|
+
if not jar_path.exists():
|
|
24
|
+
typer.echo(f"Error: {config.tla.jar_name} not found at {jar_path}", err=True)
|
|
25
|
+
typer.echo("Run 'tla download tla' first.", err=True)
|
|
26
|
+
raise typer.Exit(1)
|
|
27
|
+
|
|
28
|
+
if not modules_dir.exists():
|
|
29
|
+
typer.echo(f"Error: modules directory not found: {modules_dir}", err=True)
|
|
30
|
+
raise typer.Exit(1)
|
|
31
|
+
|
|
32
|
+
# Find all .java files in modules dir
|
|
33
|
+
java_files = list(modules_dir.rglob("*.java"))
|
|
34
|
+
if not java_files:
|
|
35
|
+
typer.echo(f"No Java source files found in {modules_dir}", err=True)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
typer.echo(f"Compiling {len(java_files)} Java files from {modules_dir} ...")
|
|
39
|
+
|
|
40
|
+
# Ensure output dir exists
|
|
41
|
+
classes_dir.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
cmd = ["javac", "-cp", str(jar_path), "-d", str(classes_dir), *[str(f) for f in java_files]]
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
result = subprocess.run(cmd, check=True, capture_output=not verbose, text=True)
|
|
47
|
+
typer.echo(f"Successfully compiled to {classes_dir}")
|
|
48
|
+
if verbose and result.stdout:
|
|
49
|
+
typer.echo(result.stdout)
|
|
50
|
+
|
|
51
|
+
# Create META-INF/services/tlc2.overrides.ITLCOverrides
|
|
52
|
+
meta_inf = classes_dir / "META-INF" / "services"
|
|
53
|
+
meta_inf.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
service_file = meta_inf / "tlc2.overrides.ITLCOverrides"
|
|
55
|
+
|
|
56
|
+
with service_file.open("w") as f:
|
|
57
|
+
f.write(f"{config.tlc.overrides_class}\n")
|
|
58
|
+
|
|
59
|
+
typer.echo(f"Created service file at {service_file}")
|
|
60
|
+
|
|
61
|
+
except subprocess.CalledProcessError as e:
|
|
62
|
+
typer.echo("Compilation failed!", err=True)
|
|
63
|
+
if e.stdout:
|
|
64
|
+
typer.echo(e.stdout, err=True)
|
|
65
|
+
if e.stderr:
|
|
66
|
+
typer.echo(e.stderr, err=True)
|
|
67
|
+
raise typer.Exit(1) from None
|
|
68
|
+
except FileNotFoundError:
|
|
69
|
+
typer.echo("Error: 'javac' not found. Ensure JDK is installed and in PATH.", err=True)
|
|
70
|
+
raise typer.Exit(1) from None
|
tla/check_java.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Check minimal Java version."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_java_version() -> str | None:
|
|
11
|
+
"""Get the installed Java version string using 'java -version'."""
|
|
12
|
+
java_executable = shutil.which("java")
|
|
13
|
+
if not java_executable:
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
# java -version prints to stderr
|
|
18
|
+
result = subprocess.run(
|
|
19
|
+
[java_executable, "-version"],
|
|
20
|
+
capture_output=True,
|
|
21
|
+
text=True,
|
|
22
|
+
timeout=5,
|
|
23
|
+
)
|
|
24
|
+
# Combine stdout and stderr just in case
|
|
25
|
+
output = result.stderr + result.stdout
|
|
26
|
+
|
|
27
|
+
# Look for version string like "1.8.0_202" or "11.0.2" or "17"
|
|
28
|
+
# Output typically starts with: openjdk version "11.0.2" ...
|
|
29
|
+
match = re.search(r'version "(\d+(\.\d+)*(_\d+)?(-\w+)?)"', output)
|
|
30
|
+
if match:
|
|
31
|
+
return match.group(1)
|
|
32
|
+
|
|
33
|
+
# Fallback for some distributions that might minimal output
|
|
34
|
+
match = re.search(r"version (\d+(\.\d+)*)", output)
|
|
35
|
+
if match:
|
|
36
|
+
return match.group(1)
|
|
37
|
+
|
|
38
|
+
except (subprocess.SubprocessError, OSError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_java_version(version_str: str) -> int:
|
|
45
|
+
"""Parse major Java version from string.
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
- "1.8.0_202" -> 8
|
|
49
|
+
- "11.0.2" -> 11
|
|
50
|
+
- "17" -> 17
|
|
51
|
+
"""
|
|
52
|
+
parts = version_str.split(".")
|
|
53
|
+
if parts[0] == "1":
|
|
54
|
+
# legacy format 1.x
|
|
55
|
+
if len(parts) > 1:
|
|
56
|
+
return int(parts[1])
|
|
57
|
+
return 1 # Fallback, though unlikely
|
|
58
|
+
return int(parts[0])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def check_java_version(min_version: int) -> None:
|
|
62
|
+
"""Check if installed Java version is at least min_version.
|
|
63
|
+
|
|
64
|
+
Exits with error if check fails.
|
|
65
|
+
"""
|
|
66
|
+
version_str = get_java_version()
|
|
67
|
+
|
|
68
|
+
if not version_str:
|
|
69
|
+
typer.echo("Error: Java is not installed or not found in PATH.", err=True)
|
|
70
|
+
typer.echo(f"Please install Java {min_version} or higher.", err=True)
|
|
71
|
+
raise typer.Exit(1)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
major_version = parse_java_version(version_str)
|
|
75
|
+
except (ValueError, IndexError):
|
|
76
|
+
typer.echo(f"Warning: Could not parse Java version from '{version_str}'. Assuming compatible.", err=True)
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
if major_version < min_version:
|
|
80
|
+
typer.echo(f"Error: Java version {min_version} or higher is required.", err=True)
|
|
81
|
+
typer.echo(f"Found version {version_str} (major version {major_version}).", err=True)
|
|
82
|
+
raise typer.Exit(1)
|
tla/cli.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""TLA+ CLI tool - entry point."""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from tla.build_tlc_module import build as build_tlc_cmd
|
|
8
|
+
from tla.check_java import check_java_version, get_java_version
|
|
9
|
+
from tla.config import load_config
|
|
10
|
+
from tla.download_tla2tools import tla as download_tla_cmd
|
|
11
|
+
from tla.run_tlc import tlc as run_tlc_cmd
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
name="tla",
|
|
15
|
+
help="TLA+ tools: download TLC, compile custom modules, run model checker.",
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
add_completion=False,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def version_callback(value: bool) -> None:
|
|
22
|
+
if value:
|
|
23
|
+
meta = importlib.metadata.metadata("tlaplus-cli")
|
|
24
|
+
typer.echo(f"{meta['Name']} v{meta['Version']}")
|
|
25
|
+
typer.echo(meta["Summary"])
|
|
26
|
+
raise typer.Exit()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.callback()
|
|
30
|
+
def root(
|
|
31
|
+
version: bool = typer.Option(
|
|
32
|
+
None,
|
|
33
|
+
"--version",
|
|
34
|
+
"-v",
|
|
35
|
+
help="Show the application's version and exit.",
|
|
36
|
+
callback=version_callback,
|
|
37
|
+
is_eager=True,
|
|
38
|
+
),
|
|
39
|
+
) -> None:
|
|
40
|
+
"""TLA+ CLI tool."""
|
|
41
|
+
# Load config early to trigger first-run copy
|
|
42
|
+
load_config()
|
|
43
|
+
if version:
|
|
44
|
+
# This branch is effectively redundant due to callback, but keeps type checker happy
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# --- Subcommands ---
|
|
49
|
+
|
|
50
|
+
# Register 'download' directly as a command, effectively 'tla download'
|
|
51
|
+
app.command(name="download")(download_tla_cmd)
|
|
52
|
+
|
|
53
|
+
app.command(name="tlc")(run_tlc_cmd)
|
|
54
|
+
app.command(name="build")(build_tlc_cmd)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command(name="check-java")
|
|
58
|
+
def check_java() -> None:
|
|
59
|
+
"""Check if Java is installed and meets the minimum version requirement."""
|
|
60
|
+
config = load_config()
|
|
61
|
+
version = get_java_version()
|
|
62
|
+
if version:
|
|
63
|
+
typer.echo(f"Detected Java version: {version}")
|
|
64
|
+
check_java_version(config.java.min_version)
|
|
65
|
+
typer.echo(f"Java version is compatible (>= {config.java.min_version}).")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main() -> None:
|
|
69
|
+
"""Entry point for [project.scripts]."""
|
|
70
|
+
app()
|
tla/config.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from functools import lru_cache
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import platformdirs
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from tla.settings import Settings
|
|
9
|
+
|
|
10
|
+
_APP_NAME = "tla"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@lru_cache(maxsize=1)
|
|
14
|
+
def load_config() -> Settings:
|
|
15
|
+
"""Load config from the user config directory.
|
|
16
|
+
|
|
17
|
+
Creates a default config on first run.
|
|
18
|
+
"""
|
|
19
|
+
cp = _ensure_config()
|
|
20
|
+
with cp.open() as f:
|
|
21
|
+
data = yaml.safe_load(f)
|
|
22
|
+
return Settings.model_validate(data)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def config_dir() -> Path:
|
|
26
|
+
"""OS-standard user config directory for this app."""
|
|
27
|
+
return Path(platformdirs.user_config_dir(_APP_NAME))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def config_path() -> Path:
|
|
31
|
+
"""Path to the user's config.yaml."""
|
|
32
|
+
return config_dir() / "config.yaml"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def cache_dir() -> Path:
|
|
36
|
+
"""OS-standard cache directory (stores tla2tools.jar)."""
|
|
37
|
+
return Path(platformdirs.user_cache_dir(_APP_NAME))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _default_config_path() -> Path:
|
|
41
|
+
"""Path to the default config shipped with the package."""
|
|
42
|
+
return Path(__file__).parent / "resources" / "default_config.yaml"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _ensure_config() -> Path:
|
|
46
|
+
"""Copy default config to user config dir if it doesn't exist yet."""
|
|
47
|
+
cp = config_path()
|
|
48
|
+
if not cp.exists():
|
|
49
|
+
cp.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
shutil.copy2(_default_config_path(), cp)
|
|
51
|
+
return cp
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def workspace_root() -> Path:
|
|
55
|
+
"""Return the resolved workspace root directory.
|
|
56
|
+
|
|
57
|
+
Relative paths are resolved from the current working directory.
|
|
58
|
+
"""
|
|
59
|
+
config = load_config()
|
|
60
|
+
ws_root = Path(config.workspace.root)
|
|
61
|
+
if not ws_root.is_absolute():
|
|
62
|
+
ws_root = (Path.cwd() / ws_root).resolve()
|
|
63
|
+
return ws_root
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Download tla2tools.jar into the cache directory."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from email.utils import parsedate_to_datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from tla.check_java import check_java_version
|
|
13
|
+
from tla.config import cache_dir, load_config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_version(jar_path: Path) -> str | None:
|
|
17
|
+
"""Run TLC to extract the version string, or None if java is unavailable."""
|
|
18
|
+
try:
|
|
19
|
+
result = subprocess.run(
|
|
20
|
+
["java", "-cp", str(jar_path), "tlc2.TLC"],
|
|
21
|
+
capture_output=True,
|
|
22
|
+
text=True,
|
|
23
|
+
timeout=15,
|
|
24
|
+
)
|
|
25
|
+
output = result.stdout + result.stderr
|
|
26
|
+
for line in output.splitlines():
|
|
27
|
+
if "Version" in line:
|
|
28
|
+
parts = line.split()
|
|
29
|
+
idx = parts.index("Version")
|
|
30
|
+
if idx + 1 < len(parts):
|
|
31
|
+
return parts[idx + 1]
|
|
32
|
+
except FileNotFoundError:
|
|
33
|
+
pass
|
|
34
|
+
except subprocess.TimeoutExpired:
|
|
35
|
+
typer.echo("Warning: Timed out trying to get TLC version.", err=True)
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _set_file_mtime(path: Path, last_modified: str) -> None:
|
|
40
|
+
"""Set file modification time from an HTTP Last-Modified header value."""
|
|
41
|
+
try:
|
|
42
|
+
dt = parsedate_to_datetime(last_modified)
|
|
43
|
+
mtime = dt.timestamp()
|
|
44
|
+
os.utime(path, (mtime, mtime))
|
|
45
|
+
except Exception as e:
|
|
46
|
+
typer.echo(f"Warning: could not set mtime: {e}", err=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def download(jar_path: Path, url: str) -> str:
|
|
50
|
+
"""Download the jar. Returns 'created', 'updated', or 'no_update'."""
|
|
51
|
+
headers: dict[str, str] = {}
|
|
52
|
+
|
|
53
|
+
if jar_path.exists():
|
|
54
|
+
mtime = jar_path.stat().st_mtime
|
|
55
|
+
dt = datetime.fromtimestamp(mtime, tz=UTC)
|
|
56
|
+
headers["If-Modified-Since"] = dt.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
|
57
|
+
|
|
58
|
+
typer.echo(f"Downloading {jar_path.name}...")
|
|
59
|
+
resp = requests.get(url, headers=headers, stream=True, timeout=120, allow_redirects=True)
|
|
60
|
+
|
|
61
|
+
if resp.status_code == 304:
|
|
62
|
+
return "no_update"
|
|
63
|
+
|
|
64
|
+
resp.raise_for_status()
|
|
65
|
+
|
|
66
|
+
jar_path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
existed = jar_path.exists()
|
|
68
|
+
|
|
69
|
+
total_size = int(resp.headers.get("content-length", 0))
|
|
70
|
+
|
|
71
|
+
with jar_path.open("wb") as f, typer.progressbar(length=total_size, label="Downloading") as progress:
|
|
72
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
73
|
+
f.write(chunk)
|
|
74
|
+
progress.update(len(chunk))
|
|
75
|
+
|
|
76
|
+
if "Last-Modified" in resp.headers:
|
|
77
|
+
_set_file_mtime(jar_path, resp.headers["Last-Modified"])
|
|
78
|
+
|
|
79
|
+
return "updated" if existed else "created"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def tla(
|
|
83
|
+
nightly: bool = typer.Option(False, "--nightly", help="Download nightly build instead of stable."),
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Download or update tla2tools.jar."""
|
|
86
|
+
|
|
87
|
+
config = load_config()
|
|
88
|
+
|
|
89
|
+
# Check Java version before proceeding
|
|
90
|
+
check_java_version(config.java.min_version)
|
|
91
|
+
|
|
92
|
+
url = config.tla.urls.nightly if nightly else config.tla.urls.stable
|
|
93
|
+
jar_path = cache_dir() / config.tla.jar_name
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
result = download(jar_path, url)
|
|
97
|
+
except requests.RequestException as e:
|
|
98
|
+
typer.echo(f"Error: could not download {config.tla.jar_name}: {e}", err=True)
|
|
99
|
+
raise typer.Exit(1) from None
|
|
100
|
+
|
|
101
|
+
version = _get_version(jar_path)
|
|
102
|
+
version_str = f" (version {version})" if version else ""
|
|
103
|
+
|
|
104
|
+
if result == "created":
|
|
105
|
+
typer.echo(f"Created {jar_path}{version_str}")
|
|
106
|
+
elif result == "updated":
|
|
107
|
+
typer.echo(f"Updated {jar_path}{version_str}")
|
|
108
|
+
else:
|
|
109
|
+
typer.echo(f"{config.tla.jar_name} is already at the latest version{version_str}")
|
tla/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
tla:
|
|
2
|
+
jar_name: tla2tools.jar
|
|
3
|
+
urls:
|
|
4
|
+
stable: https://github.com/tlaplus/tlaplus/releases/latest/download/tla2tools.jar
|
|
5
|
+
nightly: https://tla.msr-inria.inria.fr/tlatoolbox/ci/dist/tla2tools.jar
|
|
6
|
+
|
|
7
|
+
# Path to the TLA+ workspace (specs, custom modules, compiled classes).
|
|
8
|
+
# Relative paths are resolved from the current working directory.
|
|
9
|
+
workspace:
|
|
10
|
+
root: .
|
|
11
|
+
spec_dir: spec
|
|
12
|
+
modules_dir: modules
|
|
13
|
+
classes_dir: classes
|
|
14
|
+
|
|
15
|
+
tlc:
|
|
16
|
+
java_class: tlc2.TLC
|
|
17
|
+
overrides_class: tlc2.overrides.TLCOverrides
|
|
18
|
+
|
|
19
|
+
java:
|
|
20
|
+
min_version: 11
|
|
21
|
+
opts:
|
|
22
|
+
- "-XX:+IgnoreUnrecognizedVMOptions"
|
|
23
|
+
- "-XX:+UseParallelGC"
|
tla/run_tlc.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Run TLC model checker on a TLA+ specification."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from tla.check_java import check_java_version
|
|
9
|
+
from tla.config import cache_dir, load_config, workspace_root
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def tlc(
|
|
13
|
+
spec: str = typer.Argument(help="Name of the TLA+ specification (without .tla extension)."),
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Run TLC model checker on a TLA+ specification."""
|
|
16
|
+
config = load_config()
|
|
17
|
+
|
|
18
|
+
# Check Java version before proceeding
|
|
19
|
+
check_java_version(config.java.min_version)
|
|
20
|
+
|
|
21
|
+
# Jar lives in the cache directory
|
|
22
|
+
jar_path = cache_dir() / config.tla.jar_name
|
|
23
|
+
if not jar_path.exists():
|
|
24
|
+
typer.echo(f"Error: {config.tla.jar_name} not found at {jar_path}", err=True)
|
|
25
|
+
typer.echo("Run 'tla download tla' first.", err=True)
|
|
26
|
+
raise typer.Exit(1)
|
|
27
|
+
|
|
28
|
+
ws_root = workspace_root()
|
|
29
|
+
spec_dir = ws_root / config.workspace.spec_dir
|
|
30
|
+
spec_file = spec_dir / f"{spec}.tla"
|
|
31
|
+
if not spec_file.exists():
|
|
32
|
+
typer.echo(f"Error: specification not found: {spec_file}", err=True)
|
|
33
|
+
raise typer.Exit(1)
|
|
34
|
+
|
|
35
|
+
classpath = str(jar_path)
|
|
36
|
+
classes_path = ws_root / config.workspace.classes_dir
|
|
37
|
+
if classes_path.exists():
|
|
38
|
+
classpath = f"{classes_path}{os.pathsep}{classpath}"
|
|
39
|
+
|
|
40
|
+
cmd = ["java", *config.java.opts, "-cp", classpath, config.tlc.java_class, str(spec_file)]
|
|
41
|
+
|
|
42
|
+
typer.echo(f"Running TLC on {spec}.tla ...")
|
|
43
|
+
typer.echo(f"Command: {' '.join(cmd)}")
|
|
44
|
+
result = subprocess.run(cmd, cwd=str(spec_dir))
|
|
45
|
+
raise typer.Exit(result.returncode)
|
tla/settings.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, model_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TlaUrls(BaseModel):
|
|
9
|
+
stable: str
|
|
10
|
+
nightly: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TlaConfig(BaseModel):
|
|
14
|
+
jar_name: str
|
|
15
|
+
urls: TlaUrls
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WorkspaceConfig(BaseModel):
|
|
19
|
+
root: Path
|
|
20
|
+
spec_dir: Path
|
|
21
|
+
modules_dir: Path
|
|
22
|
+
classes_dir: Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TlcConfig(BaseModel):
|
|
26
|
+
java_class: str = "tlc2.TLC"
|
|
27
|
+
overrides_class: str = "tlc2.overrides.TLCOverrides"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class JavaConfig(BaseModel):
|
|
31
|
+
min_version: int = 11
|
|
32
|
+
opts: list[str] = Field(default_factory=lambda: ["-XX:+IgnoreUnrecognizedVMOptions", "-XX:+UseParallelGC"])
|
|
33
|
+
|
|
34
|
+
@model_validator(mode="before")
|
|
35
|
+
@classmethod
|
|
36
|
+
def check_env_opts(cls, data: Any) -> Any:
|
|
37
|
+
if isinstance(data, dict):
|
|
38
|
+
env_opts = os.environ.get("JAVA_OPTS")
|
|
39
|
+
if env_opts:
|
|
40
|
+
data["opts"] = env_opts.split()
|
|
41
|
+
return data
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Settings(BaseModel):
|
|
45
|
+
tla: TlaConfig
|
|
46
|
+
workspace: WorkspaceConfig
|
|
47
|
+
tlc: TlcConfig
|
|
48
|
+
java: JavaConfig = Field(default_factory=JavaConfig)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tlaplus-cli
|
|
3
|
+
Version: 0.1.7
|
|
4
|
+
Summary: TLA+ tools: download TLC, compile custom modules, run model checker
|
|
5
|
+
Author-email: Denis Nikolskiy <codeomatics@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nikolskiy/tlaplus-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/nikolskiy/tlaplus-cli
|
|
9
|
+
Project-URL: Issues, https://github.com/nikolskiy/tlaplus-cli/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/nikolskiy/tlaplus-cli/blob/main/CHANGELOG.md
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: typer<1.0.0,>=0.22.0
|
|
23
|
+
Requires-Dist: requests<3.0.0,>=2.31.0
|
|
24
|
+
Requires-Dist: pyyaml<7.0,>=6.0
|
|
25
|
+
Requires-Dist: platformdirs<5.0,>=4.0
|
|
26
|
+
Requires-Dist: pydantic<3.0.0,>=2.12.5
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# TLA+ CLI
|
|
30
|
+
|
|
31
|
+
Command-line tool for working with TLA+ specifications and the TLC model checker.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Install system-wide via uv tool
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv tool install git+https://github.com/nikolskiy/tlaplus-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Upgrade:
|
|
42
|
+
```bash
|
|
43
|
+
uv tool upgrade tlaplus-cli
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Uninstall
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv tool uninstall tlaplus-cli
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Download TLC
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Download stable release
|
|
58
|
+
tla download
|
|
59
|
+
|
|
60
|
+
# Download nightly build
|
|
61
|
+
tla download --nightly
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Check Java Version
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
tla check-java
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Compile Custom Java Modules
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
tla build
|
|
74
|
+
|
|
75
|
+
# Verbose output
|
|
76
|
+
tla build --verbose
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Compiles `.java` files from `workspace/modules/` into `workspace/classes/`.
|
|
80
|
+
|
|
81
|
+
### Run TLC
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
tla tlc <spec_name>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
For example:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
tla tlc queue
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
On first run, a default config is created at:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
~/.config/tla/config.yaml
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Edit this file to set your workspace path and TLC options:
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
tla:
|
|
105
|
+
jar_name: tla2tools.jar
|
|
106
|
+
urls:
|
|
107
|
+
stable: https://github.com/tlaplus/tlaplus/releases/latest/download/tla2tools.jar
|
|
108
|
+
nightly: https://tla.msr-inria.inria.fr/tlatoolbox/ci/dist/tla2tools.jar
|
|
109
|
+
|
|
110
|
+
workspace:
|
|
111
|
+
root: . # Project root (relative to CWD)
|
|
112
|
+
spec_dir: spec # Directory containing .tla files
|
|
113
|
+
modules_dir: modules # Directory containing .java files
|
|
114
|
+
classes_dir: classes # Directory for compiled .class files
|
|
115
|
+
|
|
116
|
+
tlc:
|
|
117
|
+
java_class: tlc2.TLC
|
|
118
|
+
overrides_class: tlc2.overrides.TLCOverrides
|
|
119
|
+
|
|
120
|
+
java:
|
|
121
|
+
min_version: 11
|
|
122
|
+
opts:
|
|
123
|
+
- "-XX:+IgnoreUnrecognizedVMOptions"
|
|
124
|
+
- "-XX:+UseParallelGC"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Directory Layout
|
|
128
|
+
|
|
129
|
+
| Directory | Purpose | Location |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| Config | `config.yaml` | `~/.config/tla/` |
|
|
132
|
+
| Cache | `tla2tools.jar` | `~/.cache/tla/` |
|
|
133
|
+
| Workspace | specs + modules + classes | Set via `workspace.root` in config |
|
|
134
|
+
|
|
135
|
+
## Note on Package Name
|
|
136
|
+
|
|
137
|
+
This package is distributed on PyPI as **`tlaplus-cli`** but imports as **`tla`**. There is a separate, unrelated [`tla`](https://pypi.org/project/tla/) package on PyPI (a TLA+ parser). If you have both installed, they will conflict. In practice this is unlikely since they serve different purposes, but be aware of it.
|
|
138
|
+
|
|
139
|
+
## Dependencies
|
|
140
|
+
|
|
141
|
+
* **Java >= 11**: Required for TLC.
|
|
142
|
+
* [**uv**](https://docs.astral.sh/uv/getting-started/installation/): For installing the tool.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
tla/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
tla/build_tlc_module.py,sha256=uclqwA4rXBydllUWKoMF6bz8T3p2Ee-Y5pE0yqgJS94,2444
|
|
3
|
+
tla/check_java.py,sha256=2XCK7lEJXZ8sLl3W3iHL-8i_0Q1yvq8KGVRJ1HYD3_4,2428
|
|
4
|
+
tla/cli.py,sha256=E6P1yJFGVzWQNeWz3gcJDycMjZQexfFD8ButQZFjSwo,1899
|
|
5
|
+
tla/config.py,sha256=89yTVrtWlQVXel7d-uCLXvpk0Mxc3jBK4Vi21-BowXU,1620
|
|
6
|
+
tla/download_tla2tools.py,sha256=VPjrxZgmyYnuEe8OylaiFrkV-SkzWQaV_YuMC5nfYXA,3581
|
|
7
|
+
tla/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
tla/run_tlc.py,sha256=QGE9NXFq3ClanXExtglv6gQPWDLjghXP7l_ckntdhSY,1517
|
|
9
|
+
tla/settings.py,sha256=EFUR4TxjUc8YMTuamX-5dPh0qgD6o_EWctcyzX-QKqM,1079
|
|
10
|
+
tla/resources/default_config.yaml,sha256=altsNmZ8YGVhZgR5NVr0wFbpasgRZ870uCfwommF9RQ,602
|
|
11
|
+
tlaplus_cli-0.1.7.dist-info/licenses/LICENSE,sha256=kSdiS8uZ4kMvuDD_2ttsaoTtFbnf6Pyv_RrNWDKFRyM,1072
|
|
12
|
+
tlaplus_cli-0.1.7.dist-info/METADATA,sha256=nCFuwu7EhN2hEaXLd4a_12sviRVBRNPZf07qEAsI6tk,3480
|
|
13
|
+
tlaplus_cli-0.1.7.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
14
|
+
tlaplus_cli-0.1.7.dist-info/entry_points.txt,sha256=W7e3tHBQcfdnUYKInVqzmkMa8mV2wg6mACw06U72k9E,37
|
|
15
|
+
tlaplus_cli-0.1.7.dist-info/top_level.txt,sha256=hPeD7gCgxcaE0qw9JQATNEfCwIEh300GKJyysU_XoS8,4
|
|
16
|
+
tlaplus_cli-0.1.7.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Denis Nikolskiy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tla
|