├── .cspell.json ├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yaml │ ├── codeql-analysis.yml │ └── pypi.yaml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── cosmofy.schema.json ├── examples ├── pkg-nested │ ├── __init__.py │ └── sub-folder │ │ ├── __main__.py │ │ └── ignore.py ├── pkg-with-init │ └── __init__.py ├── pkg-with-main │ ├── __main__.py │ ├── pkg.py │ └── py.typed └── single-file │ ├── file-no-docstring.py │ ├── file-no-main.py │ └── file-with-main.py ├── pyproject.toml ├── src └── cosmofy │ ├── __init__.py │ ├── __main__.py │ ├── args.py │ ├── bundler.py │ ├── downloader.py │ ├── py.typed │ ├── pythonoid.py │ ├── receipt.py │ ├── updater.py │ └── zipfile2.py ├── test ├── test_args.py ├── test_bundler.py ├── test_download.py ├── test_main.py ├── test_pythonoid.py ├── test_receipt.py ├── test_updater.py └── test_zipfile2.py └── uv.lock /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en", 4 | "words": [ 5 | "Autobuild", 6 | "codeql", 7 | "compresslevel", 8 | "cosmofy", 9 | "exitmsg", 10 | "fpclose", 11 | "levelname", 12 | "libc", 13 | "metaist", 14 | "mypy", 15 | "myscript", 16 | "outl", 17 | "parsedate", 18 | "pdoc", 19 | "pycache", 20 | "pycs", 21 | "pypa", 22 | "pypi", 23 | "pyright", 24 | "pytest", 25 | "PYTHONINSPECT", 26 | "pythonoid", 27 | "setuptools", 28 | "venv", 29 | "zinfo" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.py] 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | CHANGELOG.md -text merge=union 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: metaist 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: { branches: [main] } 5 | pull_request: { branches: [main] } 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | python-version: ["3.9", "3.10", "3.11", "3.12"] 14 | 15 | steps: 16 | - name: Checkout repo 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Set up caches 25 | uses: actions/cache@v4 26 | with: 27 | path: ~/.cache/pip 28 | key: ${{ runner.os }}-py${{ matrix.python-version }} 29 | 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip setuptools wheel 33 | pip install -e ".[dev]" 34 | 35 | - name: Format & Lint (ruff, cspell) 36 | run: | 37 | ds lint 38 | 39 | - name: Type check (pyright, mypy) 40 | run: | 41 | ds types 42 | 43 | - name: Run tests (pytest, coverage) 44 | run: | 45 | ds test 46 | 47 | - name: Build docs (pdoc) 48 | run: | 49 | ds docs 50 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | 10 | jobs: 11 | analyze: 12 | name: Analyze 13 | runs-on: ubuntu-latest 14 | permissions: 15 | actions: read 16 | contents: read 17 | security-events: write 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | language: ["python"] 23 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 35 | # If this step fails, then you should remove it and run the build manually (see below) 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v3 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v3 41 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yaml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | pypi-publish: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | id-token: write 13 | 14 | steps: 15 | - name: Checkout repo 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.x" 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip setuptools wheel 26 | pip install build 27 | 28 | - name: Check install 29 | run: | 30 | pip install -e . 31 | 32 | - name: Build package 33 | run: | 34 | python -m build 35 | 36 | - name: Publish package 37 | uses: pypa/gh-action-pypi-publish@2f6f737ca5f74c637829c0f5c3acd0e29ea5e8bf 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | # general 4 | _ignore* 5 | _seed.py 6 | .venv* 7 | *.db 8 | 9 | # editors 10 | .idea 11 | .vim 12 | .vscode 13 | *.code-workspace 14 | .DS_Store 15 | 16 | # test 17 | .coverage 18 | .mypy_cache 19 | .pytest_cache 20 | .ruff_cache 21 | /htmlcov 22 | 23 | # build 24 | __pycache__ 25 | .pdm-python 26 | *.egg-info 27 | /build 28 | /dist 29 | node_modules/ 30 | 31 | # rye 32 | .python-version 33 | requirements-dev.lock 34 | requirements.lock 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog] and this project adheres to [Semantic Versioning]. 6 | 7 | Sections order is: `Fixed`, `Changed`, `Added`, `Deprecated`, `Removed`, `Security`. 8 | 9 | [keep a changelog]: http://keepachangelog.com/en/1.0.0/ 10 | [semantic versioning]: http://semver.org/spec/v2.0.0.html 11 | 12 | --- 13 | 14 | ## [Unreleased] 15 | 16 | [unreleased]: https://github.com/metaist/cosmofy/compare/prod...main 17 | 18 | These are changes that are on `main` that are not yet in `prod`. 19 | 20 | --- 21 | 22 | [#1]: https://github.com/metaist/cosmofy/issues/1 23 | [#2]: https://github.com/metaist/cosmofy/issues/2 24 | [#3]: https://github.com/metaist/cosmofy/issues/3 25 | [#4]: https://github.com/metaist/cosmofy/issues/4 26 | [#5]: https://github.com/metaist/cosmofy/issues/5 27 | [#6]: https://github.com/metaist/cosmofy/issues/6 28 | [#7]: https://github.com/metaist/cosmofy/issues/7 29 | [#8]: https://github.com/metaist/cosmofy/issues/8 30 | [#9]: https://github.com/metaist/cosmofy/issues/9 31 | [#10]: https://github.com/metaist/cosmofy/issues/10 32 | [#11]: https://github.com/metaist/cosmofy/issues/11 33 | [#12]: https://github.com/metaist/cosmofy/issues/12 34 | [#13]: https://github.com/metaist/cosmofy/issues/13 35 | [0.1.0]: https://github.com/metaist/cosmofy/commits/0.1.0 36 | 37 | ## [0.1.0] - 2024-09-18T18:55:19Z 38 | 39 | Initial release. 40 | 41 | **Added** 42 | 43 | - [#1], [#4], [#12]: bootstrap cosmofy to build itself 44 | - [#2], [#6], [#8], [#9], [#13]: JSON receipt and schema 45 | - [#3], [#5]: `--receipt-url`, `--release-url`, `--release-version` 46 | - [#7]: `--self-update`, `--self-update --help`, `--self-update --version` 47 | - [#10]: release notes 48 | - [#11]: auto-upload build artifacts to GitHub Release 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Toolchain 4 | 5 | The top-level tool chain for managing this project is tested on Linux and macOS. 6 | Here are links for installing the appropriate tools. 7 | 8 | - [`cspell`](https://cspell.org/docs/installation/) 9 | - [`ds`](https://github.com/metaist/ds#install) 10 | - [`gh`](https://github.com/cli/cli#installation) 11 | - [`git`](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 12 | - [`uv`](https://github.com/astral-sh/uv#installation) 13 | 14 | All remaining tools are installed below. 15 | 16 | ## Local Development 17 | 18 | ```bash 19 | # get the code 20 | git clone git@github.com:metaist/cosmofy.git 21 | cd cosmofy 22 | uv sync --extra dev 23 | ``` 24 | 25 | Periodically, you should run: 26 | 27 | ```bash 28 | ds dev # check lint, type-checks, and run tests 29 | ``` 30 | 31 | This repo generally tries to maintain type-correctness (via `mypy` and `pyright`) and complete unit test coverage. 32 | 33 | ## Making a Release 34 | 35 | Checkout `prod`: 36 | 37 | ```bash 38 | git checkout prod 39 | git merge --no-ff --no-edit main 40 | ``` 41 | 42 | Update top-most `__init__.py`: 43 | 44 | ```python 45 | __version__ = "X.0.1" 46 | ``` 47 | 48 | Update `CHANGELOG.md`. To see recently closed issues run: 49 | 50 | ```bash 51 | ds recent-closed 52 | ``` 53 | 54 | You can also look at the [unreleased](https://github.com/metaist/cosmofy/compare/prod...main) log too. 55 | 56 | Sections order is: `Fixed`, `Changed`, `Added`, `Deprecated`, `Removed`, `Security`. 57 | 58 | ```markdown 59 | --- 60 | 61 | [X.0.1]: https://github.com/metaist/cosmofy/compare/X.0.0...X.0.1 62 | 63 | ## [X.0.1] - XXXX-XX-XXT00:00:00Z 64 | 65 | **Fixed** 66 | 67 | **Changed** 68 | 69 | **Added** 70 | 71 | **Deprecated** 72 | 73 | **Removed** 74 | 75 | **Security** 76 | ``` 77 | 78 | ### Final checks, tag, and push 79 | 80 | ```bash 81 | export VER="X.0.1" 82 | 83 | # final checks again every supported python version 84 | ds dev-all # requires uv >= 0.3.0 85 | 86 | # final build 87 | ds docs build 88 | 89 | # commit, push tags, create a new release 90 | ds release: $VER 91 | ``` 92 | 93 | [Review the release on GitHub](https://github.com/metaist/cosmofy/releases). Once published, the `pypi.yaml` workflow will attempt to publish it to PyPI. 94 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2025 Metaist LLC. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cosmofy: Cosmopolitan Python Bundler 2 | 3 |

4 | Build 5 | PyPI 6 | Supported Python Versions 7 |

8 | 9 | `cosmofy` bundles your python app into a **single executable** which runs on 10 | Linux, macOS, and Windows. It uses [Cosmopolitan libc](https://github.com/jart/cosmopolitan). 11 | 12 | ## Install 13 | 14 | macOS / Linux: 15 | 16 | ```bash 17 | dest=~/.local/bin/cosmofy 18 | curl -sSz $dest -o $dest -L https://github.com/metaist/cosmofy/releases/latest/download/cosmofy 19 | chmod +x $dest 20 | ``` 21 | 22 | Windows: (PowerShell instructions coming soon) 23 | 24 | ## Examples 25 | 26 | ```bash 27 | # bundle a script with python 28 | cosmofy myscript.py # produces `myscript.com` 29 | ./myscript.com # runs on macOS / Linux / Windows 30 | 31 | # bundle a whole directory; change output path 32 | cosmofy src/my-pkg --args '-m my-pkg --more-args' --output dist/my-pkg 33 | ./dist/my-pkg # starts bundled python with "-m my-pky --more-args" 34 | 35 | # bundle self-updater (see below) 36 | cosmofy src/my-pkg \ 37 | --release-url https://github.com/metaist/cosmofy/releases/latest/download/cosmofy 38 | ./my-pkg.com # run app as normal 39 | ./my-pkg.com --self-update # run cosmofy.updater to install any updates 40 | ``` 41 | 42 | ## Usage 43 | 44 | 48 | 49 | ```text 50 | cosmofy: Cosmopolitan Python Bundler 51 | 52 | USAGE 53 | 54 | cosmofy 55 | [--help] [--version] [--debug] [--dry-run] [--self-update] 56 | [--python-url URL] [--cache PATH] [--clone] 57 | [--output PATH] [--args STRING] 58 | ... [--exclude GLOB]... [--remove GLOB]... 59 | [--receipt PATH] [--receipt-url URL] [--release-url URL] 60 | [--release-version STRING] 61 | 62 | GENERAL 63 | 64 | -h, --help Show this help message and exit. 65 | --version Show program version and exit. 66 | --debug Show debug messages. 67 | -n, --dry-run Do not make any file system changes. 68 | --self-update Update `cosmofy` to the latest version. 69 | 70 | CACHE 71 | 72 | --python-url URL 73 | URL from which to download Cosmopolitan Python. 74 | [default: https://cosmo.zip/pub/cosmos/bin/python] 75 | [env: COSMOFY_PYTHON_URL=] 76 | 77 | --cache PATH 78 | Directory in which to cache Cosmopolitan Python downloads. 79 | Use `false` or `0` to disable caching. 80 | [default: ~/.cache/cosmofy] 81 | [env: COSMOFY_CACHE_DIR=] 82 | 83 | --clone 84 | Obtain python by cloning `cosmofy` and removing itself instead of 85 | downloading it from `--python-url`. 86 | 87 | OUTPUT 88 | 89 | -o PATH, --output PATH 90 | Path to output file. 91 | [default: `.com`] 92 | 93 | `` is the first module with a `__main__.py` or file with an 94 | `if __name__ == "__main__"` line. 95 | 96 | FILES 97 | 98 | --args STRING 99 | Cosmopolitan Python arguments. 100 | [default: `"-m "`] 101 | 102 | If NOT using the self-updater, all python options are supported: 103 | https://docs.python.org/3/using/cmdline.html 104 | 105 | If using the self-updater only a subset is supported: 106 | https://github.com/metaist/cosmofy#supported-python-cli 107 | 108 | --add GLOB, 109 | At least one glob-like patterns to add. Folders are recursively added. 110 | Files ending in `.py` will be compiled. 111 | 112 | -x GLOB, --exclude GLOB 113 | One or more glob-like patterns to exclude from being added. 114 | 115 | Common things to exclude are egg files and python cache: 116 | $ cosmofy src -x "**/*.egg-info/*" -x "**/__pycache__/*" 117 | 118 | --rm GLOB, --remove GLOB 119 | One or more glob-like patters to remove from the output. 120 | 121 | Common things to remove are `pip`, terminal info, and SSL certs: 122 | $ cosmofy src/my_module --rm 'usr/*' --rm 'Lib/site-packages/pip/*' 123 | 124 | SELF-UPDATER 125 | 126 | Specifying any of the options below will add `cosmofy.updater` 127 | to make the resulting bundle capable of updating itself. You 128 | must supply at least `--receipt-url` or `--release-url`. 129 | 130 | In addition to building the bundle, there will be a second output 131 | which is a JSON file (called a receipt) that needs to be uploaded 132 | together with the bundle. 133 | 134 | If the bundle is run with `--self-update` anywhere in the arguments, 135 | `cosmofy.updater` will run. It will compare it's internal build 136 | date with the date at `--receipt-url` and will download any updates, if 137 | they exist. 138 | 139 | Otherwise, the bundle will run as normal by calling `--args` 140 | 141 | NOTE: The updater will alter `--args` so that it gets called first. 142 | It supports most Python Command Line interface options (like `-m`). 143 | For a full list see: https://github.com/metaist/cosmofy#supported-python-cli 144 | 145 | --receipt PATH 146 | Set the path for the JSON receipt. 147 | [default: `.json`] 148 | 149 | --receipt-url URL 150 | URL to the published receipt. 151 | [default: --release-url + .json] 152 | [env: RECEIPT_URL=] 153 | 154 | --release-url URL 155 | URL to the file to download. 156 | [default: --receipt-url without .json] 157 | [env: RELEASE_URL=] 158 | 159 | --release-version STRING 160 | Release version. 161 | [default: first version-like string in `$(${output} --version)`] 162 | ``` 163 | 164 | 165 | 166 | ## Self Updater 167 | 168 | If you provide `--receipt-url` or `--release-url`, `cosmofy` will add a 169 | self-updater to the output bundle. 170 | 171 | - If the bundle is run with `--self-update` anywhere in the arguments, 172 | `cosmofy.updater` will run. It will compare it's internal build 173 | date with the date at `--receipt-url` and will download any updates, if 174 | they exist. 175 | 176 | - Otherwise, the bundle will run as normal by calling `--args`. 177 | [See below](#supported-python-cli) for minor limitations. 178 | 179 | 183 | 184 | ```text 185 | Usage: --self-update [--help] [--version] [--debug] 186 | 187 | Options: 188 | --self-update Run this updater instead of 189 | -h, --help Show this message and exit. 190 | --version Show updater version and exit. 191 | --debug Show debug messages. 192 | 193 | [env: RECEIPT_URL=] 194 | Override the embedded URL for downloading update metadata. 195 | 196 | [env: RELEASE_URL=] 197 | Override the published URL for downloading the update. 198 | ``` 199 | 200 | 201 | 202 | ## Supported Python CLI 203 | 204 | Cosmopolitan Python apps have a special `.args` file which is read when it 205 | starts up. The contents of this file are typically set by the `--args` option. 206 | However, when using the [self-updater](#self-updater), we need to check for 207 | the `--self-update` option first. 208 | 209 | If `--self-update` is NOT present, we want to process the rest of the 210 | `--args` as usual. However, since Python has already started, we only support 211 | the following [Python Command Line Interface options](https://docs.python.org/3/using/cmdline.html): 212 | 213 | - `-c `: run a command 214 | - `-m `: run a module (this is the most common) 215 | - `-`: read a command from `stdin` (rare, but we support it) 216 | - `