diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70aa6b7..6a50bbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,8 +15,8 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.11" - - run: pip install black>=24.0 - - run: black --check src/ + - run: pip install -e ".[dev]" + - run: doit format_check typecheck: name: Type check @@ -27,7 +27,7 @@ jobs: with: python-version: "3.11" - run: pip install -e ".[dev]" - - run: mypy src/ + - run: doit typecheck test: name: Tests @@ -38,7 +38,7 @@ jobs: with: python-version: "3.11" - run: pip install -e ".[dev]" - - run: pytest + - run: doit test build: name: Build (${{ matrix.os }}) @@ -53,9 +53,8 @@ jobs: with: python-version: "3.11" - run: pip install -e ".[dev]" - - run: pyinstaller --onefile src/nexus_sync/client/__main__.py --name nexus-sync-client - - run: pyinstaller --onefile src/nexus_sync/server/__main__.py --name nexus-sync-server - - run: pyinstaller --onefile src/nexus_sync/cli/__main__.py --name nexus-cli + - run: doit package + - run: doit pyinstaller - uses: actions/upload-artifact@v4 with: name: nexus-sync-${{ matrix.os }} diff --git a/.gitignore b/.gitignore index 0d8d477..29ae5ae 100644 --- a/.gitignore +++ b/.gitignore @@ -219,4 +219,6 @@ __marimo__/ .vscode/ -*.DS_Store \ No newline at end of file +*.DS_Store +.doit.db* +nexus-sync.db \ No newline at end of file diff --git a/README.md b/README.md index fcebc49..e445a4b 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,18 @@ client on its next heartbeat. ### To build +Use `doit` for repeatable checks and package builds: + +```bash +pip install -e .[dev] +doit list +doit wheel # build dist/nexus_sync-0.1.0-py3-none-any.whl +doit package # build wheel and sdist +doit # run tests, typecheck, format check, and wheel build +``` + +PyInstaller standalone binaries are still available through Makefile targets: + ``` make [client|server|cli] ``` diff --git a/docs/cli.md b/docs/cli.md index 115c0e4..cf16ce1 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -68,3 +68,15 @@ make cli ``` This creates `dist/nexus-cli` through PyInstaller. + +## Build wheel package + +Use `doit` from the development dependencies: + +```bash +pip install -e .[dev] +doit wheel +``` + +The wheel is written to `dist/nexus_sync-0.1.0-py3-none-any.whl` and installs the +`nexus-cli` console script. diff --git a/dodo.py b/dodo.py new file mode 100644 index 0000000..78d4e40 --- /dev/null +++ b/dodo.py @@ -0,0 +1,114 @@ +import sys +from pathlib import Path + +DOIT_CONFIG = { + "default_tasks": ["test", "typecheck", "format_check", "wheel"], +} + +PYTHON = sys.executable +PACKAGE_VERSION = "0.1.0" +WHEEL_TARGET = f"dist/nexus_sync-{PACKAGE_VERSION}-py3-none-any.whl" +SRC_FILES = list(Path("src").rglob("*.py")) +TEST_FILES = list(Path("tests").rglob("*.py")) +DOC_FILES = [Path("README.md"), *Path("docs").glob("*.md")] +BUILD_DEPS = [Path("pyproject.toml"), Path("README.md"), *SRC_FILES] + + +def task_test(): + return { + "actions": [f"{PYTHON} -m pytest -q"], + "file_dep": [str(path) for path in [*SRC_FILES, *TEST_FILES]], + "verbosity": 2, + } + + +def task_typecheck(): + return { + "actions": [f"{PYTHON} -m mypy src"], + "file_dep": [str(path) for path in SRC_FILES], + "verbosity": 2, + } + + +def task_format_check(): + return { + "actions": [f"{PYTHON} -m black --check ."], + "file_dep": [str(path) for path in [*SRC_FILES, *TEST_FILES, Path("dodo.py")]], + "verbosity": 2, + } + + +def task_wheel(): + return { + "actions": [f"{PYTHON} -m build --wheel"], + "file_dep": [str(path) for path in BUILD_DEPS], + "targets": [WHEEL_TARGET], + "clean": True, + "verbosity": 2, + } + + +def task_sdist(): + return { + "actions": [f"{PYTHON} -m build --sdist"], + "file_dep": [str(path) for path in BUILD_DEPS], + "targets": [f"dist/nexus_sync-{PACKAGE_VERSION}.tar.gz"], + "clean": True, + "verbosity": 2, + } + + +def task_package(): + return { + "actions": None, + "task_dep": ["wheel", "sdist"], + "verbosity": 2, + } + + +def task_pyinstaller_cli(): + return { + "actions": [ + f"{PYTHON} -m PyInstaller --onefile src/nexus_sync/cli/__main__.py --name nexus-cli" + ], + "file_dep": [str(path) for path in BUILD_DEPS], + "clean": True, + "verbosity": 2, + } + + +def task_pyinstaller_client(): + return { + "actions": [ + f"{PYTHON} -m PyInstaller --onefile src/nexus_sync/client/__main__.py --name nexus-sync-client" + ], + "file_dep": [str(path) for path in BUILD_DEPS], + "clean": True, + "verbosity": 2, + } + + +def task_pyinstaller_server(): + return { + "actions": [ + f"{PYTHON} -m PyInstaller --onefile src/nexus_sync/server/__main__.py --name nexus-sync-server" + ], + "file_dep": [str(path) for path in BUILD_DEPS], + "clean": True, + "verbosity": 2, + } + + +def task_pyinstaller(): + return { + "actions": None, + "task_dep": ["pyinstaller_client", "pyinstaller_server", "pyinstaller_cli"], + "verbosity": 2, + } + + +def task_clean_dist(): + return { + "actions": ["rm -rf dist build src/*.egg-info *.spec"], + "verbosity": 2, + } diff --git a/pyproject.toml b/pyproject.toml index d512409..da8a0a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,8 @@ name = "nexus_sync" version = "0.1.0" description = "A utility that allows you to manage your computers from a centralized server." readme = "README.md" -license = { file = "LICENSE" } +license = "MIT" +license-files = ["LICENSE"] requires-python = ">=3.11" dependencies = [ "fastapi>=0.115.0", @@ -23,6 +24,8 @@ dev = [ "httpx>=0.28.0", "black>=24.0", "pyinstaller>=6.0", + "build>=1.2.0", + "doit>=0.36.0", "mypy>=1.0", "types-PyYAML>=6.0.12", "pre-commit>=4.0" diff --git a/tests/test_dodo.py b/tests/test_dodo.py new file mode 100644 index 0000000..70ced83 --- /dev/null +++ b/tests/test_dodo.py @@ -0,0 +1,45 @@ +import importlib.util +from pathlib import Path +from typing import Any + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +def _load_dodo() -> Any: + spec = importlib.util.spec_from_file_location("dodo", PROJECT_ROOT / "dodo.py") + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _actions(task: dict[str, Any]) -> list[str]: + return [str(action) for action in task["actions"]] + + +def test_doit_has_wheel_task() -> None: + dodo = _load_dodo() + + task = dodo.task_wheel() + + assert task["verbosity"] == 2 + assert "dist/nexus_sync-0.1.0-py3-none-any.whl" in task["targets"] + assert "pyproject.toml" in task["file_dep"] + assert any("python -m build --wheel" in action for action in _actions(task)) + + +def test_doit_has_clean_dist_task() -> None: + dodo = _load_dodo() + + task = dodo.task_clean_dist() + + assert any("dist" in action for action in _actions(task)) + assert any("build" in action for action in _actions(task)) + assert any("*.egg-info" in action for action in _actions(task)) + + +def test_doit_default_tasks_include_checks_and_wheel() -> None: + dodo = _load_dodo() + + assert dodo.DOIT_CONFIG["default_tasks"] == ["test", "typecheck", "format_check", "wheel"]