Skip to main content

Python Makefile alternative

Project description

pymake

A Python Makefile alternative with dependency tracking and parallel execution.

Installation

pip install -e .

Quick Start

Create a Makefile.py in your project:

from pymake import sh, task

@task(outputs=["build/app"])
def build():
    sh("gcc -o build/app src/*.c")

@task(inputs=["build/app"])
def test():
    sh("./build/app --test")

@task()
def clean():
    sh("rm -rf build")

Run tasks:

pymake build      # Run the build task
pymake test       # Run test (builds first if needed)
pymake -B build   # Force rebuild
pymake -p check   # Run in parallel

Task Definition

Using the @task decorator

@task(inputs=["src/main.c"], outputs=["build/main.o"])
def compile():
    """Compile main.c to object file."""
    sh("gcc -c src/main.c -o build/main.o")

Touch files

Use touch for tasks that don't produce output files but should track execution:

@task(touch="build/.lint-done")
def lint():
    """Run linter."""
    sh("ruff check src/")

The touch file is created after the task runs and acts as an output for dependency tracking.

Dynamic registration

from pathlib import Path
from pymake import task

for src in Path("src").glob("*.c"):
    obj = Path("build") / (src.stem + ".o")

    def run(s=src, o=obj):
        sh(f"gcc -c {s} -o {o}")

    task.register(
        run,
        name=f"cc:{src}",
        inputs=[src],
        outputs=[obj],
    )

Note: Use default arguments (s=src, o=obj) to capture loop variables. Without this, all tasks would reference the final loop values due to Python's closure semantics.

Default task

Set a default task to run when pymake is invoked without arguments:

task.default("check")

Meta tasks

Use task functions as inputs to create aggregate tasks:

@task()
def lint():
    sh("ruff check src/")

@task()
def test():
    sh("pytest")

@task(inputs=[lint, test])
def all():
    pass

Dependency tasks run in order, each following normal run rules.

Execution Semantics

A task runs if any of these conditions are true (checked in order):

  1. Force flag: -B or --force was specified
  2. Phony target: Task has no outputs (and no touch file)
  3. Missing output: Any output file does not exist
  4. Stale output: Any input file is newer than the oldest output file

A task is skipped if:

  • All outputs exist AND no inputs are defined (nothing to compare)
  • All outputs exist AND all inputs are older than the oldest output
  • run_if callback returns False (checked after file conditions)

Output files

Outputs can be specified via outputs or touch:

@task(outputs=["build/app"])      # Explicit output file
@task(touch="build/.done")        # Touch file (auto-created after task runs)
@task()                           # Phony - always runs

The touch file is automatically created after successful execution and counts as an output.

Timestamp comparison

When comparing timestamps:

  • pymake uses the oldest output file's mtime
  • If any input is newer than this, the task runs

Input/Output validation

pymake enforces strict validation of input and output files:

  1. Before execution: Each input file must either exist OR have a task that produces it. If neither is true, an error is raised immediately.

  2. At task execution: All input files must exist when a task runs. If a producing task failed to create its outputs, dependent tasks will error.

  3. After task execution: All declared output files must exist after the task completes (excluding touch files, which are created automatically by pymake).

Custom Conditions

Use run_if for additional conditions after dependency checks:

def should_deploy():
    return os.environ.get("DEPLOY") == "1"

@task(run_if=should_deploy)
def deploy():
    sh("./deploy.sh")

Use run_if_not for the inverse (skip if condition is true):

def is_ci():
    return os.environ.get("CI") == "1"

@task(run_if_not=is_ci)
def local_only():
    """Only runs locally, skipped in CI."""
    sh("./local-setup.sh")

CLI Reference

pymake [options] [command] [targets...]

Commands:
  list [--all]       List tasks with docstrings (--all includes dynamic tasks)
  graph <target>     Output DOT graph of dependencies
  which <output>     Show reverse dependency tree for an output file
  run <targets>      Run specified targets
  help               Show help

Options:
  -f, --file FILE    Makefile path (default: Makefile.py)
  -p, --parallel     Enable parallel execution
  -j, --jobs N       Number of parallel workers
  -B, --force        Force rerun all tasks
  -q, --quiet        Suppress output

Shorthand:
  pymake build       Same as: pymake run build
  pymake build test  Same as: pymake run build test

Shell Utility

The sh() function runs shell commands:

from pymake import sh

sh("echo hello")                    # Output to terminal
output = sh("cat file", capture=True)  # Capture output
sh("might-fail", check=False)       # Don't raise on error

Dependency Graph

Generate a DOT graph for visualization:

pymake graph build | dot -Tpng > deps.png

Reverse Dependency Lookup

Find which task produces an output file and trace its dependencies:

$ pymake which final.txt
final.txt
└── finalize
      derived.txt
      final.txt
    └── process
          base.txt
          derived.txt
        └── generate_base
               base.txt

The tree shows inputs (←) and outputs (→) for each task.

Error Handling

  • Cyclic dependencies are detected and reported
  • Duplicate output files across tasks raise an error
  • Task failures stop execution and report the error
  • Missing input files (not produced by any task) raise UnproducibleInputError
  • Input files that don't exist at execution time raise MissingInputError
  • Output files not created by a task raise MissingOutputError

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hayeah_pymake-0.1.0.tar.gz (15.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hayeah_pymake-0.1.0-py3-none-any.whl (18.9 kB view details)

Uploaded Python 3

File details

Details for the file hayeah_pymake-0.1.0.tar.gz.

File metadata

  • Download URL: hayeah_pymake-0.1.0.tar.gz
  • Upload date:
  • Size: 15.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.2

File hashes

Hashes for hayeah_pymake-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f75c08efd31859db6cd958f18c62a0d758c7779b5a0543d484124194c05ac565
MD5 0572967e18b5c934a8f1f3734ebb45f2
BLAKE2b-256 53498d507983c21240f631b63cfe5ecde82f53280047a632158822b836aeace3

See more details on using hashes here.

File details

Details for the file hayeah_pymake-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for hayeah_pymake-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bc9715de8915e7e765bebf4f0d07894865197fc4ab0a1db6668c7a76b770129b
MD5 6d0a6d42b1a6ed8af4eb8c0eeeaee8af
BLAKE2b-256 57a13370aa97014e20ee1b61adad55c5b0deb68f492ad0fc5d859cd56d250834

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page