Skip to content

Commit

Permalink
add: initial bootstrap (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
metaist committed Sep 13, 2024
1 parent a14b144 commit 7f0ebbe
Show file tree
Hide file tree
Showing 12 changed files with 1,236 additions and 6 deletions.
7 changes: 6 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@
"words": [
"Autobuild",
"codeql",
"compresslevel",
"cosmofy",
"fpclose",
"levelname",
"mypy",
"pdoc",
"pypa",
"pypi",
"pyright",
"pytest",
"setuptools",
"venv"
"venv",
"zinfo",
"metaist"
]
}
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,17 @@
[Cosmopolitan apps](https://github.com/jart/cosmopolitan) are cross-platform binary files that work natively on Linux, macOS, and Windows. `cosmofy` is a tool to bundle your python project into a portable Cosmopolitan app.

```bash
pip install cosmofy # or: uv tool install cosmofy
cosmofy . # bundle the current directory
# install
dest=~/.local/bin/cosmofy
curl -sSz $dest -o $dest -L https://github.com/metaist/cosmofy/releases/latest/download/cosmofy
chmod +x $dest

# bundle a single script
cosmofy my-script.py
# => produces my-script.com

# bundle a directory
cosmofy src/cosmofy --args '-m cosmofy --cosmo'
```

## License
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ optional-dependencies = { dev = [
"build",
"cogapp",
"coverage",
"ds-run",
"mypy",
"pdoc3",
"pip",
Expand Down Expand Up @@ -133,7 +134,8 @@ dev-all.shell = """
"""

# Build
build = ["pip install -e .", "python -m build"]
cosmo = "cosmofy src/cosmofy --args '-m cosmofy --cosmo' --output dist/cosmofy"
build = ["pip install -e .", "python -m build", "cosmo"]
clean = """
rm -rf .mypy_cache;
rm -rf .pytest_cache;
Expand Down
71 changes: 71 additions & 0 deletions src/cosmofy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""cosmofy: Cosmopolitan Python Bundler"""

# std
from __future__ import annotations
from typing import List
from typing import Optional
import logging
import sys

# pkg
from .args import Args
from .args import USAGE
from .bundler import Bundler

__version__ = "0.1.0"
__pubdate__ = "unpublished"

log_normal = "%(levelname)s: %(message)s"
log_debug = "%(name)s.%(funcName)s: %(levelname)s: %(message)s"
log_verbose = " %(filename)s:%(lineno)s %(funcName)s(): %(levelname)s: %(message)s"
logging.basicConfig(level=logging.INFO, format=log_normal)

log = logging.getLogger(__name__)


def main(argv: Optional[List[str]] = None) -> int:
"""Main entry point."""
short_usage = "\n" + USAGE[USAGE.find("USAGE") + 5 : USAGE.find("GENERAL")].strip()

try:
args = Args.parse((argv or sys.argv)[1:])
except ValueError as e:
log.error(e)
print(short_usage)
return 1

if args.debug:
log.setLevel(logging.DEBUG)
formatter = logging.Formatter(log_debug)
for handler in logging.getLogger().handlers:
handler.setFormatter(formatter)
log.debug(args)

if args.version:
print(f"{__version__} ({__pubdate__})", flush=True)
return 0

if args.help:
print(USAGE)
return 0

if not args.add:
log.error("You must specify at least one path to add.")
print(short_usage)
return 1

if args.clone and not args.cosmo:
log.error(
"You cannot use --clone outside of a Cosmopolitan build. "
"See https://github.com/metaist/cosmofy#install"
)
return 1

# output = Bundler(args).run()
# TODO: generate json receipt if requested
Bundler(args).run()
return 0


if __name__ == "__main__":
sys.exit(main())
10 changes: 8 additions & 2 deletions src/cosmofy/__main__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
#!/usr/bin/env python
"""cosmofy: Cosmopolitan Python Bundler"""
"""Main entry point."""

# no cover: start
from . import main

if __name__ == "__main__":
main()
# no cover: stop
203 changes: 203 additions & 0 deletions src/cosmofy/args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
"""Command-line arguments."""

# std
from __future__ import annotations
from os import environ as ENV
from pathlib import Path
from typing import List
from typing import Optional
import dataclasses
import logging

log = logging.getLogger(__name__)

COSMOFY_PYTHON_URL = ENV.get(
"COSMOFY_PYTHON_URL", "https://cosmo.zip/pub/cosmos/bin/python"
)
"""URL to download python from."""

COSMOFY_CACHE_DIR = Path(
ENV.get("COSMOFY_CACHE_DIR", Path.home() / ".cache" / "cosmofy")
)
"""Path to cache directory."""

USAGE = f"""cosmofy: Cosmopolitan Python Bundler
USAGE
cosmofy
[--help] [--version] [--debug] [--dry-run]
[--python-url URL] [--cache PATH] [--clone]
[--output PATH] [--args STRING]
<add>... [--exclude GLOB]... [--remove GLOB]...
GENERAL
-h, --help
Show this help message and exit.
--version
Show program version and exit.
--debug
Show debug messages.
-n, --dry-run
Do not make any file system changes.
CACHE
--python-url URL
URL from which to download Cosmopolitan Python.
[env: COSMOFY_PYTHON_URL={COSMOFY_PYTHON_URL}]
--cache PATH
Directory in which to cache Cosmopolitan Python downloads.
Use `false` or `0` to disable caching.
[env: COSMOFY_CACHE_DIR={COSMOFY_CACHE_DIR}]
--clone
EXPERIMENTAL: Whether to obtain python by cloning `cosmofy` and
removing itself instead of downloading it from `--python-url`
FILES
-o PATH, --output PATH
Path to output file (see below for default value).
--args STRING
Cosmopolitan Python arguments (see below for default value).
--add GLOB, <add>
At least one glob-like patterns to add. Folders are recursively added.
Files ending in `.py` will be compiled.
-x GLOB, --exclude GLOB
One or more glob-like patterns to exclude from being added.
By default, "*.egg-info" and "__pycache__" are excluded.
--rm GLOB, --remove GLOB
One or more glob-like patters to remove from the output.
Common things to remove are `pip`, terminal info, and SSL certs:
$ cosmofy src/my_module --rm 'usr/*' --rm 'Lib/site-packages/pip/*'
NOTES
When `--args` or `--output` is missing:
- If `<path>` is a single file:
--args = "-m <path_without_suffix>"
--output = "<path_without_suffix>.com"
- If `<path>` contains a `__main__.py`, the first one encountered:
--args = "-m <parent_folder>"
--output = "<parent_folder>.com"
- If `<path>` contains a `__init__.py`, we search for the first file
that contains the line `if __name__ == '__main__'`:
--args = "-m <file_without_suffix>"
--output = "<file_without_suffix>.com"
"""


@dataclasses.dataclass
class Args:
help: bool = False
"""Whether to show usage."""

version: bool = False
"""Whether to show version."""

debug: bool = False
"""Whether to show debug messages."""

cosmo: bool = False
"""Whether we are running inside a Cosmopolitan build."""

dry_run: bool = False
"""Whether we should suppress any file-system operations."""

for_real: bool = True
"""Internal value for the opposite of `dry_run`."""

# cache

python_url: str = COSMOFY_PYTHON_URL
"""URL from which to download Cosmopolitan Python."""

cache: Optional[Path] = COSMOFY_CACHE_DIR
"""Directory for caching downloads."""

clone: bool = False
"""Whether to clone `cosmofy` to get python."""

# files

args: str = ""
"""Args to pass to Cosmopolitan python."""

output: Optional[Path] = None
"""Path to the output file."""

paths: List[Path] = dataclasses.field(default_factory=list)
"""Paths to add."""

add: List[str] = dataclasses.field(default_factory=list)
"""Globs to add."""

exclude: List[str] = dataclasses.field(default_factory=list)
"""Globs to exclude."""

remove: List[str] = dataclasses.field(default_factory=list)
"""Globs to remove."""

@staticmethod
def parse(argv: List[str]) -> Args:
args = Args()
alias = {
"-h": "--help",
"-n": "--dry-run",
"-o": "--output",
"-x": "--exclude",
"--rm": "--remove",
}
while argv:
if argv[0].startswith("-"):
arg = argv.pop(0)
arg = alias.get(arg, arg)
else:
arg = "--add"
prop = arg[2:].replace("-", "_")

# bool
if arg in [
"--clone",
"--cosmo",
"--debug",
"--dry-run",
"--help",
"--version",
]:
setattr(args, prop, True)

# str
elif arg in ["--args", "--python-url"]:
setattr(args, prop, argv.pop(0))

# path
elif arg in ["--cache", "--output"]:
setattr(args, prop, Path(argv.pop(0)))

# list[str]
elif arg in ["--add", "--exclude", "--remove"]:
getattr(args, prop).append(argv.pop(0))

# unknown
else:
raise ValueError(f"Unknown argument: {arg}")

args.for_real = not args.dry_run
if args.cache and args.cache.name.lower() in ["0", "false"]:
args.cache = None
return args
Loading

0 comments on commit 7f0ebbe

Please sign in to comment.