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
4 changes: 3 additions & 1 deletion src/groundhog_hpc/configuration/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ class UvMetadata(BaseModel, extra="allow", serialize_by_alias=True):
exclude_newer: str | None = Field(
default_factory=_default_exclude_newer, alias="exclude-newer"
)
python_preference: str | None = Field(default="managed", alias="python-preference")
python_preference: str | None = Field(
default="only-managed", alias="python-preference"
)
index_url: str | None = Field(default=None, alias="index-url")
extra_index_url: list[str] | None = Field(default=None, alias="extra-index-url")
python_downloads: str | None = Field(default=None, alias="python-downloads")
Expand Down
22 changes: 19 additions & 3 deletions src/groundhog_hpc/templates/shell_command.sh.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,31 @@ else
fi
{% endraw %}

"$UV_BIN" venv "$ENV_DIR"{% if requires_python %} --python "{{ requires_python }}"{% endif %}
{% if uv_config_toml %}

# Both `uv venv` and `uv pip install` will receive --config-file, then
# the file is moved into $ENV_DIR as a human-readable audit trail.
{% raw %}UV_TOML="${{ENV_DIR}}.uv.toml"{% endraw %}
mkdir -p "$(dirname "$UV_TOML")"

cat > "$UV_TOML" << 'UV_CONFIG_EOF'
{{ uv_config_toml | escape_braces }}
UV_CONFIG_EOF
{% endif %}

"$UV_BIN" venv "$ENV_DIR"{% if requires_python %} --python "{{ requires_python }}"{% endif %}{% if uv_config_toml %} --config-file "$UV_TOML"{% endif %}

{% if uv_config_toml %}
mv "$UV_TOML" "$ENV_DIR/uv.toml"
{% endif %}

# Install dependencies
"$UV_BIN" pip install --python "$ENV_DIR/bin/python" \
{% if exclude_newer %}--exclude-newer "{{ exclude_newer }}" {% endif %}\
{% if uv_config_toml %}--config-file "$ENV_DIR/uv.toml" {% endif %}\
--exclude-newer-package groundhog-hpc={{ groundhog_timestamp }} \
{% for dep in dependencies %}"{{ dep }}" {% endfor %}{{ version_spec }}

# Write metadata for debugging
# Write metadata for posterity
cat > "$ENV_DIR/groundhog-meta.json" << 'META_EOF'
{{ '{{' }}
"created_at": "{{ groundhog_timestamp }}",
Expand Down
23 changes: 20 additions & 3 deletions src/groundhog_hpc/templating.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from hashlib import sha1
from pathlib import Path

import tomlkit
from jinja2 import Environment, FileSystemLoader

from groundhog_hpc.configuration.models import Pep723Metadata
Expand Down Expand Up @@ -149,6 +150,8 @@ def template_shell_command(script_path: str, function_name: str, payload: str) -
local_log_level = local_log_level.upper()
logger.debug(f"Propagating log level to remote: {local_log_level}")

uv_config_toml = _serialize_uv_toml(metadata)

# Render shell command
shell_template = jinja_env.get_template("shell_command.sh.jinja")
shell_command_string = shell_template.render(
Expand All @@ -165,16 +168,30 @@ def template_shell_command(script_path: str, function_name: str, payload: str) -
groundhog_version=groundhog_version,
requires_python=metadata.requires_python if metadata else "",
dependencies=metadata.dependencies if metadata else [],
exclude_newer=metadata.tool.uv.exclude_newer
if metadata and metadata.tool and metadata.tool.uv
else None,
uv_config_toml=uv_config_toml,
)

logger.debug(f"Generated shell command ({len(shell_command_string)} chars)")

return shell_command_string


def _serialize_uv_toml(metadata: Pep723Metadata | None) -> str:
"""Serialize [tool.uv] settings to uv.toml format for uv pip install.

Returns a TOML string containing all non-None settings from the user's
[tool.uv] block, or an empty string if there are no settings.
"""
if not metadata or not metadata.tool or not metadata.tool.uv:
return ""

uv_dict = metadata.tool.uv.model_dump(by_alias=True, exclude_none=True)
if not uv_dict:
return ""

return tomlkit.dumps(uv_dict).strip()


def _script_hash_prefix(contents: str, length: int = 8) -> str:
return str(sha1(bytes(contents, "utf-8")).hexdigest()[:length])

Expand Down
Loading
Loading