Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/slua_bundle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: slua_bundle

on:
push:
branches: [main, develop]
paths:
- 'tools/slua_bundle/**'
- '.github/workflows/slua_bundle.yml'
pull_request:
paths:
- 'tools/slua_bundle/**'
- '.github/workflows/slua_bundle.yml'

defaults:
run:
working-directory: tools/slua_bundle

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install ruff
- run: ruff check .

typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install --upgrade pip
- run: pip install . --group dev
- run: mypy slua_bundle

test:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: ["3.7", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install . pytest
- run: pytest
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,10 @@ __pycache__
/fuzz/corpus/json/*
!/fuzz/corpus/json/*.json
*.code-workspace

# Python tooling
*.egg-info/
.pytest_cache/
.mypy_cache/
.ruff_cache/
*.swp
419 changes: 419 additions & 0 deletions rfcs/static-require-bundle.md

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions tools/slua_bundle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# slua-bundle

Reference implementation of the SLua static-require-bundle algorithm. Bundle a Luau project rooted at a `MAIN` module into a single text artifact, inspect bundle artifacts, and extract bundles back into a self-contained project tree.

For the format spec and design rationale, see [`rfcs/static-require-bundle.md`](../../rfcs/static-require-bundle.md) at the repo root.

## Install

Requires Python 3.7+.

```
pip install ./tools/slua_bundle
```

Or, for development:

```
pip install -e ./tools/slua_bundle
```

Either form puts a `slua-bundle` command on `$PATH`.

## CLI

### `slua-bundle bundle`

Bundle a project tree into a single text artifact. Project-internal modules canonicalize under the built-in `@root` alias. `--project` is an optional advisory project name emitted as the bundle's `PROJECT` directive (for viewer-side linkage to a disk project); it does not affect canonicalization.

```
slua-bundle bundle --root ./src ./src/Main.luau -o bundle.lua
slua-bundle bundle --root ./src --project myhud ./src/Main.luau -o bundle.lua
```

Omit `-o` to write to stdout.

Pass `--input-bundle PATH` to use an existing bundle as a last-resort resolver: when a require's source is missing on disk (or its alias isn't declared in `.luaurc`), the embedded copy from the prior bundle is used instead. Disk wins when both are available. This is the rebundle-without-source flow - useful when a CLI receives a viewer-built bundle and needs to rebuild it without access to the original resolvers (inventory, etc.).

### `slua-bundle inspect`

Show a bundle's structure: format version, advisory project name (if any), MAIN entry point, and a per-module byte breakdown sorted by canonical key.

```
slua-bundle inspect bundle.lua
```

### `slua-bundle extract`

Reverse a bundle into a project tree. `@root` modules land flat under `<output>/`, non-root aliases under `<output>/<alias>/`. A `.luaurc` is generated when non-root aliases are present. Re-bundling the extracted tree reproduces the original bundle byte-for-byte.

```
slua-bundle extract -o ./extracted bundle.lua
```

## Library

```python
from pathlib import Path
from slua_bundle import DiskFS, bundle, parse_bundle
from slua_bundle.extractor import extract_to_dir

fs = DiskFS(Path("./src"))
text = bundle(fs, project_root=Path("./src").resolve(),
main_path=Path("./src/Main.luau").resolve())
# Optionally pass project_name="myhud" to emit a PROJECT directive.

parsed = parse_bundle(text)
extract_to_dir(parsed, DiskFS(Path("./extracted")), Path("./extracted").resolve())
```

`MemoryFS` is also available for dict-backed in-memory use, mirroring `DiskFS`.

## Development

```
pip install -e ./tools/slua_bundle
cd tools/slua_bundle
python -m pytest
```
64 changes: 64 additions & 0 deletions tools/slua_bundle/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "slua-bundle"
version = "0.1.0"
description = "Reference implementation of the SLua static-require-bundle algorithm: bundle, inspect, and extract Luau projects."
requires-python = ">=3.7"
readme = "README.md"

[project.scripts]
slua-bundle = "slua_bundle.cli:main"

[tool.setuptools]
packages = ["slua_bundle"]

[tool.setuptools.package-data]
slua_bundle = ["py.typed"]

[dependency-groups]
dev = [
"mypy>=1.0",
"pytest>=8",
"pytest-cov>=6",
"ruff>=0.8",
]
test = [
"pytest>=8",
"pytest-cov",
]

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]

[tool.ruff]
line-length = 100
target-version = "py37"

[tool.ruff.lint]
select = ["B", "C", "E", "F", "W", "I", "C90", "RUF"]
ignore = ["E501", "RUF012"]

[tool.ruff.lint.isort]
combine-as-imports = true

[tool.ruff.lint.mccabe]
max-complexity = 25

[tool.mypy]
follow_imports = "normal"
strict_optional = true

[tool.coverage.run]
source = ["slua_bundle"]
branch = true

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]
63 changes: 63 additions & 0 deletions tools/slua_bundle/slua_bundle/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from .bundler import (
AmbiguousResolutionError,
DepthExceededError,
MarkerInjectionError,
ModuleCountExceededError,
NoResolverError,
bundle,
)
from .canonicalize import (
AliasCollisionWarning,
NoCoveringAliasError,
ReservedAliasError,
canonicalize,
)
from .errors import BundleError
from .fs import DiskFS, FSBackend, MemoryFS
from .luaurc import InvalidLuaurcError
from .resolver import (
BareIdentifierError,
InvalidPathComponentError,
RelativeRequireWithoutAnchorError,
RequireEscapesAliasError,
UnknownAliasError,
resolve,
)
from .runtime import (
BundleParseError,
BundleParser,
CircularDependencyError,
ParsedBundle,
parse_bundle,
simulate,
)

__all__ = [
"AliasCollisionWarning",
"AmbiguousResolutionError",
"BareIdentifierError",
"BundleError",
"BundleParseError",
"BundleParser",
"CircularDependencyError",
"DepthExceededError",
"DiskFS",
"FSBackend",
"InvalidLuaurcError",
"InvalidPathComponentError",
"MarkerInjectionError",
"MemoryFS",
"ModuleCountExceededError",
"NoCoveringAliasError",
"NoResolverError",
"ParsedBundle",
"RelativeRequireWithoutAnchorError",
"RequireEscapesAliasError",
"ReservedAliasError",
"UnknownAliasError",
"bundle",
"canonicalize",
"parse_bundle",
"resolve",
"simulate",
]
5 changes: 5 additions & 0 deletions tools/slua_bundle/slua_bundle/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import sys

from .cli import main

sys.exit(main())
Loading
Loading