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
2 changes: 1 addition & 1 deletion .copier-answers.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Changes here will be overwritten by Copier
_commit: v0.0.44
_commit: v0.0.49
_src_path: gh:LabAutomationAndScreening/copier-base-template.git
description: Copier template for creating Python libraries and executables
python_ci_versions:
Expand Down
5 changes: 4 additions & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
FROM mcr.microsoft.com/vscode/devcontainers/universal:2.9.0-focal
# base image tags available at https://mcr.microsoft.com/v2/devcontainers/universal/tags/list
# added the platform flag to override any local settings since this image is only compatible with linux/amd64. since this image is only x64 compatible, suppressing the hadolint rule
# hadolint ignore=DL3029
FROM --platform=linux/amd64 mcr.microsoft.com/devcontainers/universal:2.13.1-focal

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

Expand Down
26 changes: 14 additions & 12 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
"service": "devcontainer",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"features": {
"ghcr.io/devcontainers/features/aws-cli:1": {
"ghcr.io/devcontainers/features/aws-cli:1.1.1": {
// https://github.com/devcontainers/features/blob/main/src/aws-cli/devcontainer-feature.json
// view latest version https://raw.githubusercontent.com/aws/aws-cli/v2/CHANGELOG.rst
"version": "2.24.24"
"version": "2.27.14"
},
"ghcr.io/devcontainers/features/python:1": {
// https://github.com/devcontainers/features/tree/main/src/python
"ghcr.io/devcontainers/features/python:1.7.1": {
// https://github.com/devcontainers/features/blob/main/src/python/devcontainer-feature.json
"version": "3.12.7",
"installTools": false,
"optimize": true
Expand All @@ -20,16 +21,16 @@
"extensions": [
// basic tooling
"eamodio.gitlens@15.5.1",
"ms-vscode.live-server@0.5.2024091601",
"ms-vscode.live-server@0.5.2025051301",
"MS-vsliveshare.vsliveshare@1.0.5905",
"github.copilot@1.304.1523",
"github.copilot-chat@0.27.2025042301",
"github.copilot@1.320.1564",
"github.copilot-chat@0.28.2025051402",

// Python
"ms-python.python@2024.14.1",
"ms-python.vscode-pylance@2024.9.2",
"ms-vscode-remote.remote-containers@0.383.0",
"charliermarsh.ruff@2024.54.0",
"ms-python.python@2025.7.2025051401",
"ms-python.vscode-pylance@2025.4.104",
"ms-vscode-remote.remote-containers@0.414.0",
"charliermarsh.ruff@2025.22.0",

// Misc file formats
"bierner.markdown-mermaid@1.28.0",
Expand All @@ -42,6 +43,7 @@
"editor.accessibilitySupport": "off", // turn off sounds
"extensions.autoUpdate": false,
"extensions.autoCheckUpdates": false,
"livePreview.portNumber": 3025, // arbitrary not to conflict with default 3000 Nuxt port number
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff"
Expand All @@ -59,5 +61,5 @@
"initializeCommand": "sh .devcontainer/initialize-command.sh",
"onCreateCommand": "sh .devcontainer/on-create-command.sh",
"postStartCommand": "sh .devcontainer/post-start-command.sh"
// Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): a16b1f65 # spellchecker:disable-line
// Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): dfcd01a5 # spellchecker:disable-line
}
7 changes: 7 additions & 0 deletions .devcontainer/envs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"description": "main",
"relative_directory": ".",
"package_manager": "uv"
}
]
114 changes: 114 additions & 0 deletions .devcontainer/install-ci-tooling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import argparse
import os
import platform
import shutil
import subprocess
import sys

UV_VERSION = "0.7.8"
PNPM_VERSION = "10.11.0"
COPIER_VERSION = "9.7.1"
COPIER_TEMPLATES_EXTENSION_VERSION = "0.3.1"
PRE_COMMIT_VERSION = "4.2.0"
GITHUB_WINDOWS_RUNNER_BIN_PATH = r"C:\Users\runneradmin\.local\bin"
parser = argparse.ArgumentParser(description="Install CI tooling for the repo")
_ = parser.add_argument(
"--no-python",
default=False,
action="store_true",
help="Do not process any environments using python package managers",
)
_ = parser.add_argument(
"--python-version",
default=f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
type=str,
help="What version to install.",
)
_ = parser.add_argument(
"--no-node", action="store_true", default=False, help="Do not process any environments using node package managers"
)


def main():
args = parser.parse_args(sys.argv[1:])
is_windows = platform.system() == "Windows"
uv_env = dict(os.environ)
uv_env.update({"UV_PYTHON_PREFERENCE": "only-system", "UV_PYTHON": args.python_version})
uv_path = ((GITHUB_WINDOWS_RUNNER_BIN_PATH + "\\") if is_windows else "") + "uv"
if is_windows:
pwsh = shutil.which("pwsh") or shutil.which("powershell")
if not pwsh:
raise FileNotFoundError("Neither 'pwsh' nor 'powershell' found on PATH")
if not args.no_python:
if is_windows:
uv_env.update({"PATH": rf"{GITHUB_WINDOWS_RUNNER_BIN_PATH};{uv_env['PATH']}"})
# invoke installer in a pwsh process
_ = subprocess.run(
[
pwsh, # type: ignore[reportPossiblyUnboundVariable] # this matches the conditional above that defines pwsh
"-NoProfile",
"-NonInteractive",
"-Command",
f"irm https://astral.sh/uv/{UV_VERSION}/install.ps1 | iex",
],
check=True,
env=uv_env,
)
else:
_ = subprocess.run(
f"curl -fsSL https://astral.sh/uv/{UV_VERSION}/install.sh | sh",
check=True,
shell=True,
env=uv_env,
)
# TODO: add uv autocompletion to the shell https://docs.astral.sh/uv/getting-started/installation/#shell-autocompletion
_ = subprocess.run(
[
uv_path,
"tool",
"install",
f"copier=={COPIER_VERSION}",
"--with",
f"copier-templates-extensions=={COPIER_TEMPLATES_EXTENSION_VERSION}",
],
check=True,
env=uv_env,
)
_ = subprocess.run(
[
uv_path,
"tool",
"install",
f"pre-commit=={PRE_COMMIT_VERSION}",
],
check=True,
env=uv_env,
)
_ = subprocess.run(
[
uv_path,
"tool",
"list",
],
check=True,
env=uv_env,
)
if not args.no_node:
pnpm_install_sequence = ["npm -v", f"npm install -g pnpm@{PNPM_VERSION}", "pnpm -v"]
for cmd in pnpm_install_sequence:
cmd = (
[
pwsh, # type: ignore[reportPossiblyUnboundVariable] # this matches the conditional above that defines pwsh
"-NoProfile",
"-NonInteractive",
"-Command",
cmd,
]
if is_windows
else [cmd]
)
_ = subprocess.run(cmd, shell=True, check=True)


if __name__ == "__main__":
main()
26 changes: 0 additions & 26 deletions .devcontainer/install-ci-tooling.sh

This file was deleted.

120 changes: 120 additions & 0 deletions .devcontainer/manual-setup-deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import argparse
import enum
import json
import os
import platform
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any

REPO_ROOT_DIR = Path(__file__).parent.parent.resolve()
ENVS_CONFIG = REPO_ROOT_DIR / ".devcontainer" / "envs.json"
parser = argparse.ArgumentParser(description="Manual setup for dependencies in the repo")
_ = parser.add_argument(
"--python-version",
type=str,
default=f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
help="What version to install.",
)
_ = parser.add_argument("--skip-check-lock", action="store_true", default=False, help="Skip the lock file check step")
_ = parser.add_argument(
"--optionally-check-lock", action="store_true", default=False, help="Check the lock file IFF it exists"
)
_ = parser.add_argument(
"--no-python",
action="store_true",
default=False,
help="Do not process any environments using python package managers",
)
_ = parser.add_argument(
"--no-node", action="store_true", default=False, help="Do not process any environments using node package managers"
)


class PackageManager(str, enum.Enum):
UV = "uv"
PNPM = "pnpm"


class EnvConfig:
def __init__(self, json_dict: dict[str, Any]):
super().__init__()
self.package_manager = PackageManager(json_dict["package_manager"])
self.path = REPO_ROOT_DIR
if "relative_directory" in json_dict:
self.path = REPO_ROOT_DIR / json_dict["relative_directory"]
if self.package_manager == PackageManager.UV:
self.lock_file = self.path / "uv.lock"
elif self.package_manager == PackageManager.PNPM:
self.lock_file = self.path / "pnpm-lock.yaml"
else:
raise NotImplementedError(f"Package manager {self.package_manager} is not supported")


def main():
args = parser.parse_args(sys.argv[1:])
is_windows = platform.system() == "Windows"
uv_env = dict(os.environ)
uv_env.update({"UV_PYTHON_PREFERENCE": "only-system", "UV_PYTHON": args.python_version})
skip_check_lock = args.skip_check_lock
if skip_check_lock and args.optionally_check_lock:
print("Cannot skip and optionally check the lock file at the same time.")
sys.exit(1)

with ENVS_CONFIG.open("r") as f:
envs = json.load(f)

for env_dict in envs:
env = EnvConfig(env_dict)
if args.no_python and env.package_manager == PackageManager.UV:
print(f"Skipping environment {env.path} as it uses a Python package manager and --no-python is set")
continue
if args.no_node and env.package_manager == PackageManager.PNPM:
print(f"Skipping environment {env.path} as it uses a Node package manager and --no-node is set")
continue
env_skip_check_lock = skip_check_lock
if args.optionally_check_lock and env.lock_file.exists():
env_skip_check_lock = False
if not env_skip_check_lock:
if env.package_manager == PackageManager.UV:
_ = subprocess.run(["uv", "lock", "--check", "--directory", str(env.path)], check=True, env=uv_env)
elif env.package_manager == PackageManager.PNPM:
pass # doesn't seem to be a way to do this https://github.com/orgs/pnpm/discussions/3202
else:
raise NotImplementedError(f"Package manager {env.package_manager} does not support lock file checking")
if env.package_manager == PackageManager.UV:
sync_command = ["uv", "sync", "--directory", str(env.path)]
if not env_skip_check_lock:
sync_command.append("--frozen")
_ = subprocess.run(
sync_command,
check=True,
env=uv_env,
)
elif env.package_manager == PackageManager.PNPM:
pnpm_command = ["pnpm", "install", "--dir", str(env.path)]
if not env_skip_check_lock:
pnpm_command.append("--frozen-lockfile")
if is_windows:
pwsh = shutil.which("pwsh") or shutil.which("powershell")
if not pwsh:
raise FileNotFoundError("Neither 'pwsh' nor 'powershell' found on PATH")
pnpm_command = [
pwsh,
"-NoProfile",
"-NonInteractive",
"-Command",
" ".join(pnpm_command),
]
_ = subprocess.run(
pnpm_command,
check=True,
)
else:
raise NotImplementedError(f"Package manager {env.package_manager} is not supported for installation")


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion .devcontainer/on-create-command-boilerplate.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash
set -ex

sh .devcontainer/install-ci-tooling.sh
python .devcontainer/install-ci-tooling.py

git config --global --add --bool push.autoSetupRemote true
git config --local core.symlinks true
Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/on-create-command.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ sh .devcontainer/on-create-command-boilerplate.sh

pre-commit install --install-hooks

sh .devcontainer/manual-setup-deps.sh --optionally-lock
python .devcontainer/manual-setup-deps.py --optionally-check-lock
Loading