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
57 changes: 43 additions & 14 deletions src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,10 @@ private static IResourceBuilder<PythonAppResource> AddPythonAppCore(
_ => throw new InvalidOperationException($"Unsupported entrypoint type: {entrypointType}")
};

// Check if uv.lock exists in the working directory
var uvLockPath = Path.Combine(resource.WorkingDirectory, "uv.lock");
var hasUvLock = File.Exists(uvLockPath);

var builderStage = context.Builder
.From($"ghcr.io/astral-sh/uv:python{pythonVersion}-bookworm-slim", "builder")
.EmptyLine()
Expand All @@ -406,20 +410,45 @@ private static IResourceBuilder<PythonAppResource> AddPythonAppCore(
.Env("UV_LINK_MODE", "copy")
.EmptyLine()
.WorkDir("/app")
.EmptyLine()
.Comment("Install dependencies first for better layer caching")
.Comment("Uses BuildKit cache mounts to speed up repeated builds")
.RunWithMounts(
"uv sync --locked --no-install-project --no-dev",
"type=cache,target=/root/.cache/uv",
"type=bind,source=uv.lock,target=uv.lock",
"type=bind,source=pyproject.toml,target=pyproject.toml")
.EmptyLine()
.Comment("Copy the rest of the application source and install the project")
.Copy(".", "/app")
.RunWithMounts(
"uv sync --locked --no-dev",
"type=cache,target=/root/.cache/uv");
.EmptyLine();

if (hasUvLock)
{
// If uv.lock exists, use locked mode for reproducible builds
builderStage
.Comment("Install dependencies first for better layer caching")
.Comment("Uses BuildKit cache mounts to speed up repeated builds")
.RunWithMounts(
"uv sync --locked --no-install-project --no-dev",
"type=cache,target=/root/.cache/uv",
"type=bind,source=uv.lock,target=uv.lock",
"type=bind,source=pyproject.toml,target=pyproject.toml")
.EmptyLine()
.Comment("Copy the rest of the application source and install the project")
.Copy(".", "/app")
.RunWithMounts(
"uv sync --locked --no-dev",
"type=cache,target=/root/.cache/uv");
}
else
{
// If uv.lock doesn't exist, copy pyproject.toml and generate lock file
builderStage
.Comment("Copy pyproject.toml to install dependencies")
.Copy("pyproject.toml", "/app/")
.EmptyLine()
.Comment("Install dependencies and generate lock file")
.Comment("Uses BuildKit cache mount to speed up repeated builds")
.RunWithMounts(
"uv sync --no-install-project --no-dev",
"type=cache,target=/root/.cache/uv")
.EmptyLine()
.Comment("Copy the rest of the application source and install the project")
.Copy(".", "/app")
.RunWithMounts(
"uv sync --no-dev",
"type=cache,target=/root/.cache/uv");
}

var runtimeBuilder = context.Builder
.From($"python:{pythonVersion}-slim-bookworm", "app")
Expand Down
70 changes: 70 additions & 0 deletions tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,76 @@ await Verify(scriptDockerfileContent)
.AppendContentAsFile(executableDockerfileContent);
}

[Fact]
public async Task WithUvEnvironment_GeneratesDockerfileInPublishMode_WithoutUvLock()
{
using var sourceDir = new TempDirectory();
using var outputDir = new TempDirectory();
var projectDirectory = sourceDir.Path;

// Create a UV-based Python project with pyproject.toml but NO uv.lock
var pyprojectContent = """
[project]
name = "test-app"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
""";

var scriptContent = """
print("Hello from UV project!")
""";

File.WriteAllText(Path.Combine(projectDirectory, "pyproject.toml"), pyprojectContent);
// Note: NO uv.lock file created
File.WriteAllText(Path.Combine(projectDirectory, "main.py"), scriptContent);

var manifestPath = Path.Combine(projectDirectory, "aspire-manifest.json");

using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, publisher: "manifest", outputPath: outputDir.Path);

// Add Python resources with different entrypoint types
builder.AddPythonScript("script-app", projectDirectory, "main.py")
.WithUvEnvironment();

builder.AddPythonModule("module-app", projectDirectory, "mymodule")
.WithUvEnvironment();

builder.AddPythonExecutable("executable-app", projectDirectory, "pytest")
.WithUvEnvironment();

var app = builder.Build();

app.Run();

// Verify that Dockerfiles were generated for each entrypoint type
var scriptDockerfilePath = Path.Combine(outputDir.Path, "script-app.Dockerfile");
Assert.True(File.Exists(scriptDockerfilePath), "Dockerfile should be generated for script entrypoint");

var moduleDockerfilePath = Path.Combine(outputDir.Path, "module-app.Dockerfile");
Assert.True(File.Exists(moduleDockerfilePath), "Dockerfile should be generated for module entrypoint");

var executableDockerfilePath = Path.Combine(outputDir.Path, "executable-app.Dockerfile");
Assert.True(File.Exists(executableDockerfilePath), "Dockerfile should be generated for executable entrypoint");

var scriptDockerfileContent = File.ReadAllText(scriptDockerfilePath);
var moduleDockerfileContent = File.ReadAllText(moduleDockerfilePath);
var executableDockerfileContent = File.ReadAllText(executableDockerfilePath);

// Verify the Dockerfiles don't use --locked flag
Assert.DoesNotContain("--locked", scriptDockerfileContent);
Assert.DoesNotContain("--locked", moduleDockerfileContent);
Assert.DoesNotContain("--locked", executableDockerfileContent);

await Verify(scriptDockerfileContent)
.AppendContentAsFile(moduleDockerfileContent)
.AppendContentAsFile(executableDockerfileContent);
}

[Fact]
public async Task WithVSCodeDebugSupport_RemovesScriptArgumentForScriptEntrypoint()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder

# Enable bytecode compilation and copy mode for the virtual environment
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy

WORKDIR /app

# Copy pyproject.toml to install dependencies
COPY pyproject.toml /app/

# Install dependencies and generate lock file
# Uses BuildKit cache mount to speed up repeated builds
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --no-install-project --no-dev

# Copy the rest of the application source and install the project
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --no-dev

FROM python:3.12-slim-bookworm AS app

# ------------------------------
# 🚀 Runtime stage
# ------------------------------
# Create non-root user for security
RUN groupadd --system --gid 999 appuser && useradd --system --gid 999 --uid 999 --create-home appuser

# Copy the application and virtual environment from builder
COPY --from=builder --chown=appuser:appuser /app /app

# Add virtual environment to PATH and set VIRTUAL_ENV
ENV PATH=/app/.venv/bin:${PATH}
ENV VIRTUAL_ENV=/app/.venv
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Use the non-root user to run the application
USER appuser

# Set working directory
WORKDIR /app

# Run the application
ENTRYPOINT ["python","main.py"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder

# Enable bytecode compilation and copy mode for the virtual environment
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy

WORKDIR /app

# Copy pyproject.toml to install dependencies
COPY pyproject.toml /app/

# Install dependencies and generate lock file
# Uses BuildKit cache mount to speed up repeated builds
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --no-install-project --no-dev

# Copy the rest of the application source and install the project
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --no-dev

FROM python:3.12-slim-bookworm AS app

# ------------------------------
# 🚀 Runtime stage
# ------------------------------
# Create non-root user for security
RUN groupadd --system --gid 999 appuser && useradd --system --gid 999 --uid 999 --create-home appuser

# Copy the application and virtual environment from builder
COPY --from=builder --chown=appuser:appuser /app /app

# Add virtual environment to PATH and set VIRTUAL_ENV
ENV PATH=/app/.venv/bin:${PATH}
ENV VIRTUAL_ENV=/app/.venv
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Use the non-root user to run the application
USER appuser

# Set working directory
WORKDIR /app

# Run the application
ENTRYPOINT ["python","-m","mymodule"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder

# Enable bytecode compilation and copy mode for the virtual environment
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy

WORKDIR /app

# Copy pyproject.toml to install dependencies
COPY pyproject.toml /app/

# Install dependencies and generate lock file
# Uses BuildKit cache mount to speed up repeated builds
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --no-install-project --no-dev

# Copy the rest of the application source and install the project
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --no-dev

FROM python:3.12-slim-bookworm AS app

# ------------------------------
# 🚀 Runtime stage
# ------------------------------
# Create non-root user for security
RUN groupadd --system --gid 999 appuser && useradd --system --gid 999 --uid 999 --create-home appuser

# Copy the application and virtual environment from builder
COPY --from=builder --chown=appuser:appuser /app /app

# Add virtual environment to PATH and set VIRTUAL_ENV
ENV PATH=/app/.venv/bin:${PATH}
ENV VIRTUAL_ENV=/app/.venv
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Use the non-root user to run the application
USER appuser

# Set working directory
WORKDIR /app

# Run the application
ENTRYPOINT ["pytest"]
Loading