Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions crates/hm-dsl-engine/harmont-py/harmont/_cmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@

import re
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, overload
from typing import TYPE_CHECKING, Any, Self, overload

from ._toolchain import make_install_chain
from ._toolchain import advance_install, make_install_chain
from .cache import CacheForever, CacheOnChange

if TYPE_CHECKING:
Expand Down Expand Up @@ -232,6 +232,28 @@ class CMakeToolchain:
ccache: bool
generator: str

def setup(
self,
cmd: str,
*,
cwd: str | None = None,
label: str | None = None,
cache: CachePolicy | None = None,
env: dict[str, str] | None = None,
) -> Self:
"""Append a post-install command and return an advanced toolchain; chainable.

Use for prep steps the toolchain's actions must depend on but that the SDK
does not model natively — code generation, fixtures, extra tooling. Every
``CMakeProject`` spawned from the returned toolchain forks from this step,
so prep runs before the configure+build.

Examples:
>>> import harmont as hm
>>> tc = hm.cmake().setup("./scripts/gen-headers.sh")
"""
return advance_install(self, cmd, cwd=cwd, label=label, cache=cache, env=env)

def project(
self,
*,
Expand Down
29 changes: 26 additions & 3 deletions crates/hm-dsl-engine/harmont-py/harmont/_elixir.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
import re
from dataclasses import dataclass
from dataclasses import field as dataclass_field
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Self

from ._toolchain import make_install_chain
from .cache import CacheForever, CacheOnChange
from ._toolchain import advance_install, make_install_chain
from .cache import CacheForever, CacheOnChange, CachePolicy

if TYPE_CHECKING:
from ._step import Step
Expand Down Expand Up @@ -73,6 +73,29 @@ class ElixirProject:
installed: Step
_plt_step: Step | None = dataclass_field(default=None, init=False, repr=False)

def setup(
self,
cmd: str,
*,
cwd: str | None = None,
label: str | None = None,
cache: CachePolicy | None = None,
env: dict[str, str] | None = None,
) -> Self:
"""Append a post-install command and return an advanced project; chainable.

Use for prep steps the toolchain's actions must depend on but that the SDK
does not model natively — code generation, fixtures, extra tooling. The
returned object's action methods (``compile``/``test``/…) fork from this
step, so they all see its results.

Examples:
>>> import harmont as hm
>>> proj = hm.elixir(path="elixir").setup("mix proto.gen")
>>> hm.pipeline([proj.compile(), proj.test()])
"""
return advance_install(self, cmd, cwd=cwd, label=label, cache=cache, env=env)

def _emit(self, cmd: str, default_label: str, **kw: Any) -> Step:
if kw.get("label") is None:
kw["label"] = default_label
Expand Down
26 changes: 24 additions & 2 deletions crates/hm-dsl-engine/harmont-py/harmont/_go.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@

import re
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Self

from ._toolchain import make_install_chain
from ._toolchain import advance_install, make_install_chain
from .cache import CacheForever

if TYPE_CHECKING:
from ._step import Step
from .cache import CachePolicy

APT_PACKAGES = ("curl", "ca-certificates", "git")

Expand Down Expand Up @@ -46,6 +47,27 @@ class GoToolchain:
path: str
installed: Step

def setup(
self,
cmd: str,
*,
cwd: str | None = None,
label: str | None = None,
cache: CachePolicy | None = None,
env: dict[str, str] | None = None,
) -> Self:
"""Append a post-install command and return an advanced toolchain; chainable.

Use for prep steps the toolchain's actions must depend on but that the SDK
does not model natively — code generation, fixtures, extra tooling. The
returned object's action methods fork from this step.

Examples:
>>> import harmont as hm
>>> tc = hm.go(path=".").setup("go generate ./...")
"""
return advance_install(self, cmd, cwd=cwd, label=label, cache=cache, env=env)

def _emit(self, cmd: str, default_label: str, **kw: Any) -> Step:
if kw.get("label") is None:
kw["label"] = default_label
Expand Down
25 changes: 24 additions & 1 deletion crates/hm-dsl-engine/harmont-py/harmont/_js.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@

import re
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Literal
from typing import TYPE_CHECKING, Any, Literal, Self

from ._detect import detect
from ._toolchain import (
advance_install,
bun_install_cmd,
deno_install_cmd,
make_install_chain,
Expand All @@ -34,6 +35,7 @@

if TYPE_CHECKING:
from ._step import Step
from .cache import CachePolicy

Runtime = Literal["node", "bun", "deno"]
PackageManager = Literal["npm", "pnpm", "yarn-classic", "yarn-berry", "bun", "deno"]
Expand Down Expand Up @@ -124,6 +126,27 @@ class JsProject:
run_prefix: str
tag: str

def setup(
self,
cmd: str,
*,
cwd: str | None = None,
label: str | None = None,
cache: CachePolicy | None = None,
env: dict[str, str] | None = None,
) -> Self:
"""Append a post-install command and return an advanced project; chainable.

Use for prep steps the toolchain's actions must depend on but that the SDK
does not model natively — code generation, fixtures, extra tooling. The
returned object's action methods fork from this step.

Examples:
>>> import harmont as hm
>>> proj = hm.js.project(path=".").setup("npm run codegen")
"""
return advance_install(self, cmd, cwd=cwd, label=label, cache=cache, env=env)

def install(self) -> Step:
"""Return the dependency-install step (the unambiguous default leaf)."""
return self.installed
Expand Down
26 changes: 24 additions & 2 deletions crates/hm-dsl-engine/harmont-py/harmont/_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@

import re
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Self

from ._toolchain import make_install_chain
from ._toolchain import advance_install, make_install_chain
from .cache import CacheForever, CacheOnChange

if TYPE_CHECKING:
from ._step import Step
from .cache import CachePolicy

APT_PACKAGES = ("curl", "ca-certificates", "python3", "python3-venv")

Expand Down Expand Up @@ -60,6 +61,27 @@ class PythonToolchain:
path: str
installed: Step # uv-sync Step

def setup(
self,
cmd: str,
*,
cwd: str | None = None,
label: str | None = None,
cache: CachePolicy | None = None,
env: dict[str, str] | None = None,
) -> Self:
"""Append a post-install command and return an advanced toolchain; chainable.

Use for prep steps the toolchain's actions must depend on but that the SDK
does not model natively — code generation, fixtures, extra tooling. The
returned object's action methods fork from this step.

Examples:
>>> import harmont as hm
>>> tc = hm.python(path=".").setup("uv run python scripts/codegen.py")
"""
return advance_install(self, cmd, cwd=cwd, label=label, cache=cache, env=env)

def _emit(self, cmd: str, default_label: str, **kw: Any) -> Step:
if kw.get("label") is None:
kw["label"] = default_label
Expand Down
26 changes: 24 additions & 2 deletions crates/hm-dsl-engine/harmont-py/harmont/_rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
import re
import shlex
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Self

from ._cargo import CargoOpts, cargo_flags
from ._toolchain import make_install_chain
from ._toolchain import advance_install, make_install_chain
from .cache import CacheForever, CacheOnChange

if TYPE_CHECKING:
Expand Down Expand Up @@ -151,6 +151,28 @@ class RustToolchain:
path: str
installed: Step

def setup(
self,
cmd: str,
*,
cwd: str | None = None,
label: str | None = None,
cache: CachePolicy | None = None,
env: dict[str, str] | None = None,
) -> Self:
"""Append a post-install command and return an advanced toolchain; chainable.

Use for prep steps the toolchain's actions must depend on but that the SDK
does not model natively — code generation, fixtures, extra tooling. The
returned object's action methods (and projects spawned from it) fork from
this step, so prep runs before the cargo warmup precompile.

Examples:
>>> import harmont as hm
>>> tc = hm.rust.toolchain().setup("cargo install sqlx-cli")
"""
return advance_install(self, cmd, cwd=cwd, label=label, cache=cache, env=env)

def _wrap(self, cargo: str) -> str:
return f". $HOME/.cargo/env && cd {self.path} && {cargo}"

Expand Down
35 changes: 34 additions & 1 deletion crates/hm-dsl-engine/harmont-py/harmont/_toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@

from __future__ import annotations

import dataclasses
from datetime import timedelta
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Protocol, TypeVar

from ._step import scratch
from .cache import CacheTTL
Expand All @@ -27,6 +28,38 @@
APT_TTL = timedelta(days=1)


class _HasInstalled(Protocol):
# Read-only member (property form) so frozen-dataclass toolchains, whose
# `installed` field is read-only, satisfy the protocol. A bare
# `installed: Step` annotation declares a *writable* member, which frozen
# instances do not match.
@property
def installed(self) -> Step: ...


_ProjectT = TypeVar("_ProjectT", bound="_HasInstalled")


def advance_install(
project: _ProjectT,
cmd: str,
*,
cwd: str | None = None,
label: str | None = None,
cache: CachePolicy | None = None,
env: dict[str, str] | None = None,
) -> _ProjectT:
"""Return a copy of a toolchain object with one command appended to its
install chain. Every action method emitted from the returned object forks
from the new step. Shared implementation behind each toolchain's ``setup()``.
"""
new_installed = project.installed.sh(cmd, cwd=cwd, label=label, cache=cache, env=env)
# All callers are frozen dataclasses carrying an `installed: Step` field, but
# the Protocol bound cannot express "is a dataclass", so the replace below
# cannot satisfy its DataclassInstance upper bound — hence the narrow ignore.
return dataclasses.replace(project, installed=new_installed) # ty: ignore[invalid-argument-type]


def apt_install_cmd(packages: tuple[str, ...]) -> str:
"""Single shell string: ``apt-get update && apt-get install -y <pkgs>``."""
pkgs = " ".join(packages)
Expand Down
48 changes: 46 additions & 2 deletions crates/hm-dsl-engine/harmont-py/harmont/_zig.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@

import re
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, overload
from typing import TYPE_CHECKING, Any, Self, overload

from ._toolchain import make_install_chain
from ._toolchain import advance_install, make_install_chain
from .cache import CacheForever

if TYPE_CHECKING:
from ._step import Step
from .cache import CachePolicy

APT_PACKAGES = ("curl", "ca-certificates", "xz-utils")

Expand Down Expand Up @@ -66,6 +67,27 @@ class ZigProject:
path: str
installed: Step

def setup(
self,
cmd: str,
*,
cwd: str | None = None,
label: str | None = None,
cache: CachePolicy | None = None,
env: dict[str, str] | None = None,
) -> Self:
"""Append a post-install command and return an advanced project; chainable.

Use for prep steps the toolchain's actions must depend on but that the SDK
does not model natively — code generation, fixtures, extra tooling. The
returned object's action methods fork from this step.

Examples:
>>> import harmont as hm
>>> proj = hm.zig(path=".").setup("zig build gen")
"""
return advance_install(self, cmd, cwd=cwd, label=label, cache=cache, env=env)

def _emit(self, cmd: str, default_label: str, **kw: Any) -> Step:
if kw.get("label") is None:
kw["label"] = default_label
Expand Down Expand Up @@ -106,6 +128,28 @@ class ZigToolchain:
version: str
installed: Step

def setup(
self,
cmd: str,
*,
cwd: str | None = None,
label: str | None = None,
cache: CachePolicy | None = None,
env: dict[str, str] | None = None,
) -> Self:
"""Append a post-install command and return an advanced toolchain; chainable.

Use for prep steps the toolchain's actions must depend on but that the SDK
does not model natively — code generation, fixtures, extra tooling. Every
``ZigProject`` spawned from the returned toolchain forks from this step.

Examples:
>>> import harmont as hm
>>> tc = hm.zig().setup("zig build gen")
>>> hm.pipeline([tc.project("lib-a").test()])
"""
return advance_install(self, cmd, cwd=cwd, label=label, cache=cache, env=env)

def project(self, path: str = ".") -> ZigProject:
"""Create a ``ZigProject`` rooted at ``path`` from this toolchain.

Expand Down
Loading
Loading