Skip to content

Nuke - Simple Task Runner

Nuke is a lightweight task runner built into Nuclear that lets you define and run tasks as Python functions in a nukefile.py script.

It's perfect for: - Running build, test, and deployment tasks - Managing development workflows - Quick automation scripts - Managing project tasks with configuration

Quick Start

Create a nukefile.py file with a few tasks:

#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
#   "nuclear>=2.8.1",
# ]
# ///
from nuclear import nuke, logger

class Config:
    src_path: str = '/opt/dump'
    dst_path: str = '/media/user/DRIVE/'
    dry: bool = False

config, sh = nuke.init(Config)

def push():
    sh(f"rsync -avh --delete --size-only --info=progress2 '{src_path}/' '{dst_path}'")

def convert():
    sh(f'ffmpeg -i "{src_path}/input.mp4" "{dst_path}/audio.mp3"')

if __name__ == '__main__':
    nuke.run()

Run tasks from command line:

nuke push
# or
./nukefile.py push

# dry run mode
nuke push --dry

# override config parameters
nuke convert --src_path=/src/dump

List all available tasks:

nuke # in the same folder where nukefile.py is
./nukefile.py

Configuration

Minimal Configuration

For simple tasks with just the dry flag:

from nuclear import nuke

config, sh = nuke.init()

def build():
    sh<<"gcc build.c"

if __name__ == '__main__':
    nuke.run()

Custom Configuration

Extend nuke.NukeConfig to add custom configuration fields:

from nuclear import nuke
from pathlib import Path

class Config(nuke.NukeConfig):
    output_dir: str = 'dist'
    num_workers: int = 4
    api_url: str = 'https://api.example.com'

config, sh = nuke.init(Config, raw_output=True, print_log=True)

def build():
    """Build project with custom output directory"""
    sh << f"make build OUTPUT={config.output_dir}"

def test():
    """Run tests with worker count"""
    sh << f"pytest -n {config.num_workers} tests/"

Loading Configuration from Files

Nuke automatically loads configuration from .config.yaml in the current directory:

output_dir: build
num_workers: 8
api_url: https://staging.example.com
dry: false

CLI arguments override file configuration:

./nukefile.py --output-dir=/tmp/build --num-workers=16 build

Shell Runner

The shell runner (sh) from nuke.init() provides convenient methods for running shell commands:

Basic Usage

from nuclear import nuke

config, sh = nuke.init()

def deploy():
    # All these are equivalent ways to run shell commands:
    sh("make deploy")
    sh << "make deploy"
    sh / "make deploy"

Shell Options

Pass options to nuke.init() to configure how commands are executed:

config, sh = nuke.init(
    Config,
    raw_output=True,      # Let subprocess manage stdout/stderr
    print_log=True,       # Print log message before running command
    print_stdout=False,   # Print live stdout as it runs
    workdir=None,         # Set working directory
    independent=False,    # Run independent process
)

Dry-Run Support

Tasks automatically support dry-run mode if your config has a dry field:

class Config(nuke.NukeConfig):
    dry: bool = False

config, sh = nuke.init(Config)

def deploy():
    sh << "rsync -avh dist/ /srv/www/"

Run in dry-mode:

python nukefile.py --dry deploy

This will log the command without executing it.

Task Dependencies

Just call the dependencies tasks explicitly and mark the funcions with functools.cache to make sure they would run just once.

from functools import cache

from nuclear import nuke

config, sh = nuke.init()

@cache
def clean():
    sh / "rm -rf dist/ build/"

@cache
def build():
    clean()
    sh / "make build"

@cache
def test():
    build()
    sh / "pytest tests/"

def deploy():
    test()
    sh / "rsync -avh dist/ /srv/www/"

if __name__ == '__main__':
    nuke.run()

When you run python nukefile.py deploy, it will automatically run: 1. clean() (dependency of build) 2. build() (dependency of test) 3. test() (dependency of deploy) 4. deploy() (requested task)

Each task runs only once, even if multiple tasks depend on it.

Validating Sources

Use nuke.validate_sources() to validate and convert file paths:

from pathlib import Path
from nuclear import nuke

class Config(nuke.NukeConfig):
    sources: list[str] = [
        '/path/to/file1.txt',
        '/path/to/file2.txt',
    ]

config, sh = nuke.init(Config)

def process():
    """Process source files"""
    files: list[Path] = nuke.validate_sources(config.sources)
    for f in files:
        logger.info(f'Processing {f}')

This function: - Converts strings to Path objects - Checks for duplicate entries - Verifies files exist - Raises helpful errors if anything is wrong

Error Handling

Nuke tasks use Nuclear's error handling system. Catch specific errors with:

from nuclear import nuke, CommandError, logger

config, sh = nuke.init()

def cleanup():
    """Cleanup (may fail if nothing exists)"""
    try:
        sh / "rm -rf /tmp/cache"
    except CommandError as e:
        logger.warn('Cleanup failed', error=str(e))

def deploy():
    sh / "make deploy"

Logging

Use logger to output status messages:

from nuclear import nuke, logger

config, sh = nuke.init()

def build():
    logger.info('Starting build', output_dir=config.output_dir)
    sh / "make build"
    logger.info('Build completed')

Advanced Example

Here's a more complete example with multiple configs, dependencies, and features:

#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
#   "nuclear>=2.8.1",
#   "unidecode",
# ]
# ///
from pathlib import Path
from nuclear import nuke, logger, CommandError
from unidecode import unidecode

class Config(nuke.NukeConfig):
    source_files: list[str] = [
        '/opt/dump/movies-series/Bluey/S01/S01E22.mkv',
    ]
    output_offset: int = 200
    output_dir: str = 'output'

config, sh = nuke.init(Config, raw_output=True, print_log=True)

def clean():
    """Remove output directory"""
    sh / f"rm -rf {config.output_dir}"

@nuke.depends('clean')
def prepare():
    """Create output directories"""
    sh / f"mkdir -p {config.output_dir}/stories {config.output_dir}/custom"

@nuke.depends('prepare')
def process_stories():
    """Process story audio files"""
    sources: list[Path] = nuke.validate_sources(config.source_files)

    for i, f in enumerate(sources):
        filename = unidecode(f.stem)
        output = f'{config.output_dir}/stories/{config.output_offset + i} {filename}.mp3'

        if Path(output).exists():
            logger.debug('File exists, skipping', file=output)
            continue

        logger.info('Processing', source=f.name, output=output)
        sh(
            f'ffmpeg -i "{f.absolute()}" '
            f'-map 0:a:1 '
            f'-ss 00:00:25 '
            f'-filter:a "aresample=matrix_encoding=dplii" '
            f'-ac 2 -q:a 0 '
            f'"{output}"'
        )

def sync_usb():
    """Sync to USB drive"""
    logger.info('Syncing to USB', dry=config.dry)
    sh / "rsync -avh --delete --size-only /opt/dump/output/ /media/usb/"

def format_drive():
    """Format and sort FAT partition"""
    try:
        partition = '/dev/sda1'
        input(f"Press enter to confirm formatting {partition}...")
        sh / f"sudo fsck.vfat -r {partition}"
        sh / f"sudo fatsort -n {partition}"
        logger.info('Drive formatted and sorted')
    except CommandError as e:
        logger.error('Format failed', error=str(e))

if __name__ == '__main__':
    nuke.run()

Usage:

# List all tasks
python nukefile.py

# Run a single task
python nukefile.py process_stories

# Run in dry-run mode
python nukefile.py --dry process_stories

# Override config values
python nukefile.py --output-offset=300 --output-dir=dist process_stories

# Run with dependencies
python nukefile.py sync_usb  # automatically runs prepare, process_stories first

Tips & Tricks

Making nukefile.py Executable

Add a shebang and make it executable:

chmod +x nukefile.py
./nukefile.py build

Or use uv to run with dependencies:

#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
#   "nuclear>=2.8.1",
# ]
# ///

Using External Dependencies

With uv inline script mode, you can add external dependencies:

#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
#   "nuclear>=2.8.1",
#   "unidecode",
#   "pyyaml",
# ]
# ///
from nuclear import nuke
from unidecode import unidecode

Logging Command Output

Use print_log=True to see what commands are being executed:

config, sh = nuke.init(Config, print_log=True)

Capturing Command Output

The shell runner returns command output:

version = sh / "python --version"
logger.info('Python version', version=version.strip())

Task with Parameters from Config

Use configuration values in your tasks:

class Config(nuke.NukeConfig):
    build_dir: str = 'dist'
    parallel_jobs: int = 4

config, sh = nuke.init(Config)

def test():
    """Run parallel tests"""
    sh / f"pytest -n {config.parallel_jobs} tests/"

def build():
    """Build with custom directory"""
    sh / f"make build OUTPUT={config.build_dir}"