Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
65f4d19
build_manager: shorten build ids and name archives vehicle-board-id
shiv-tyagi Jul 25, 2026
d89285c
builder: use renamed archive path and match tar inner folder
shiv-tyagi Jul 25, 2026
d922c9a
web: serve artifact download using archive basename
shiv-tyagi Jul 25, 2026
4a77ba3
build_manager: store selected features as API labels
shiv-tyagi Jul 25, 2026
5d18e3f
builder: resolve feature labels to defines for extra_hwdef
shiv-tyagi Jul 25, 2026
b7daf38
web: submit and return feature labels without define mapping
shiv-tyagi Jul 25, 2026
be7f66e
tests: cover label storage and resolve_feature_defines
shiv-tyagi Jul 25, 2026
f0f405f
schemas: add custombuild config schema
shiv-tyagi Jul 25, 2026
6fa34a4
build_config: add shared config YAML helpers
shiv-tyagi Jul 25, 2026
73bea96
build_manager: store display fields on BuildInfo for config YAML
shiv-tyagi Jul 25, 2026
230049c
builder: pack custombuild.yaml into the build archive
shiv-tyagi Jul 25, 2026
39cd8a8
web: add builds config download and wire submit-time display metadata
shiv-tyagi Jul 25, 2026
0c30263
tests: cover build_config helpers and builds config endpoint
shiv-tyagi Jul 25, 2026
b22d62a
backend: rename web directory to backend
shiv-tyagi Jul 25, 2026
8f23f87
frontend: add React UI
shiv-tyagi Jul 25, 2026
ea1d90c
docker-compose: rename app to backend and add frontend service
shiv-tyagi Jul 25, 2026
d5b837d
.gitignore: ignore frontend dependencies and build output
shiv-tyagi Jul 25, 2026
56e45e3
README: document backend rename and frontend setup
shiv-tyagi Jul 25, 2026
0f35f6f
.flake8: exclude frontend/node_modules from lint
shiv-tyagi Jul 25, 2026
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
3 changes: 2 additions & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ exclude =
env,
base/,
redis_data/,
.vagrant/
.vagrant/,
frontend/node_modules/
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-24.04

env:
PYTHONPATH: ${{ github.workspace }}:${{ github.workspace }}/web
PYTHONPATH: ${{ github.workspace }}:${{ github.workspace }}/backend

steps:
- name: Checkout code
Expand All @@ -27,7 +27,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest
pip install -r tests/requirements.txt
pip install -r web/requirements.txt
pip install -r backend/requirements.txt

- name: Run tests
run: |
Expand Down
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,12 @@ dump.rdb

# base directory
base

# TypeScript incremental build info
*.tsbuildinfo

# Node / frontend build artifacts
node_modules/

# Generated from repo-root schemas/config by Vite
frontend/public/schemas/
49 changes: 25 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ To minimize setup overhead and enhance ease of use, running this application in
sudo docker compose up -d
```

This starts Redis, the backend API, the builder, and the frontend. Only the frontend is published on the host; nginx serves the UI and proxies `/api` to the backend.

**Note:** When starting the application for the first time, it takes some time to initialize the ArduPilot Git repositories at the backend. This process also involves populating the list of available versions and releases using the GitHub API, so please be patient.

5. **Access the Web Interface:**
The application binds to port 11080 on your host machine by default. Open your web browser and go to `http://localhost:11080` to interact with the web interface. To change the port, set the `WEB_PORT` environment variable in the .env file mentioned in the _Configure Environment Variables_ section.
The frontend binds to port 11080 on your host machine by default. Open your web browser and go to `http://localhost:11080` to interact with the web interface. To change the port, set the `WEB_PORT` environment variable in the `.env` file mentioned in the _Configure Environment Variables_ section.

6. **Stopping the Application:**
To stop the application, you can use the following command:
Expand All @@ -61,7 +63,7 @@ To minimize setup overhead and enhance ease of use, running this application in
This will stop and remove the containers, but it will not delete any built images or volumes, preserving your data for future use.

## Running Locally Without Docker on Ubuntu
To run the ArduPilot Custom Firmware Builder locally without Docker, ensure you have an environment capable of building ArduPilot. Refer to the [ArduPilot Environment Setup Guide](https://ardupilot.org/dev/docs/building-setup-linux.html) if necessary.
This setup is intended for **local development** only, not production. Ensure you have an environment capable of building ArduPilot. Refer to the [ArduPilot Environment Setup Guide](https://ardupilot.org/dev/docs/building-setup-linux.html) if necessary.

1. **Clone the Custom-Build Repository:**
```bash
Expand All @@ -86,7 +88,7 @@ To run the ArduPilot Custom Firmware Builder locally without Docker, ensure you

3. **Install Dependencies:**
```bash
pip install -r web/requirements.txt -r builder/requirements.txt
pip install -r backend/requirements.txt -r builder/requirements.txt
```

If pip is not installed, run:
Expand All @@ -104,46 +106,45 @@ To run the ArduPilot Custom Firmware Builder locally without Docker, ensure you
sudo systemctl status redis-server
```

5. **Execute the Application:**
- For a development environment with auto-reload, run:
```bash
python3 web/main.py
```
To change the port, use the `--port` argument:
```bash
python3 web/main.py --port 9000
```
- For a production environment, use:
```bash
uvicorn web.main:app --host 0.0.0.0 --port 8080
```
5. **Start the Backend and Frontend:**
In one terminal, start the API (listens on port 8080 by default):
```bash
python3 backend/main.py
```
To use a different port, pass `--port` or set `BACKEND_PORT`. If you change it, update the Vite proxy target in `frontend/vite.config.ts` to match.

During the coding and testing phases, use the development environment to easily debug and make changes with auto-reload enabled. When deploying the app for end users, use the production environment to ensure better performance, scalability, and security.
In another terminal, start the UI:
```bash
cd frontend
npm install
npm run dev
```

The application will automatically set up the required base directory at `./base` upon first execution. You may customize this path by setting the `CBS_BASEDIR` environment variable.
The application will automatically set up the required base directory at `./base` upon first execution. You may customize this path by setting the `CBS_BASEDIR` environment variable.

6. **Access the Web Interface:**

Once the application is running, you can access the interface in your web browser at http://localhost:8080.

The default port is 8080, or the value of the `WEB_PORT` environment variable if set. You can override this by passing the `--port` argument when running the application directly (e.g., `python3 web/main.py --port 9000`) or when using uvicorn (e.g., `uvicorn web.main:app --port 5000`). Refer to the [uvicorn documentation](https://www.uvicorn.org/) for additional configuration options.
Open `http://localhost:5173` in your browser. Vite proxies `/api` requests to the backend, so you do not need nginx for local development.

## Directory Structure
The default directory structure is established as follows:
```
/home/<username>
└── CustomBuild
├── schemas
│ └── config
│ └── 0.0.1.json (shared CustomBuild YAML schema)
└── base
├── ardupilot (used by the web component)
├── artifacts
├── ardupilot (used by the backend)
├── artifacts (build bundles include custombuild.yaml)
├── configs
| └── remotes.json (optional, see examples/remotes.json.sample)
├── secrets
| └── admin_token (optional)
├── tmp
└── ardupilot (used by the builder component)
```
The build artifacts are organized under the `base/artifacts` subdirectory.
The build artifacts are organized under the `base/artifacts` subdirectory. Each completed build archive (`.tar.gz`) includes firmware binaries, `build.log`, `extra_hwdef.dat`, and a Builder-generated `custombuild.yaml` for rebuilding. Config schemas live under `schemas/config/` at the repo root (consumed by Builder, backend, and frontend).

## Acknowledgements
This project includes many valuable contributions made during the Google Summer of Code 2021. For more information, please see the [GSOC 2021 Blog Post](https://discuss.ardupilot.org/t/gsoc-2021-custom-firmware-builder/74946).
2 changes: 1 addition & 1 deletion web/Dockerfile → backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ RUN groupadd -g 999 ardupilot && \
chown ardupilot:ardupilot /app

COPY --chown=ardupilot:ardupilot . /app
WORKDIR /app/web
WORKDIR /app/backend
RUN pip install --no-cache-dir -r requirements.txt

ENV PYTHONPATH=/app
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion web/api/v1/__init__.py → backend/api/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""API v1 module."""
from web.api.v1.router import router
from backend.api.v1.router import router

__all__ = ["router"]
4 changes: 2 additions & 2 deletions web/api/v1/admin.py → backend/api/v1/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from fastapi import APIRouter, HTTPException, Depends, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

from web.schemas import RefreshVersionsResponse
from web.services.admin import get_admin_service, AdminService
from backend.schemas import RefreshVersionsResponse
from backend.services.admin import get_admin_service, AdminService


router = APIRouter(prefix="/admin", tags=["admin"])
Expand Down
59 changes: 54 additions & 5 deletions web/api/v1/builds.py → backend/api/v1/builds.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from typing import List, Optional
from fastapi import (
APIRouter,
Expand All @@ -8,15 +9,15 @@
Depends,
Request
)
from fastapi.responses import FileResponse, PlainTextResponse
from fastapi.responses import FileResponse, PlainTextResponse, Response

from web.schemas import (
from backend.schemas import (
BuildRequest,
BuildSubmitResponse,
BuildOut,
)
from web.services.builds import get_builds_service, BuildsService
from web.core.limiter import limiter
from backend.services.builds import get_builds_service, BuildsService
from backend.core.limiter import limiter

router = APIRouter(prefix="/builds", tags=["builds"])

Expand Down Expand Up @@ -219,5 +220,53 @@ async def download_artifact(
return FileResponse(
path=artifact_path,
media_type='application/gzip',
filename=f"{build_id}.tar.gz"
filename=os.path.basename(artifact_path)
)


@router.get(
"/{build_id}/config",
responses={
404: {
"description": (
"Build not found or config could not be generated"
)
}
}
)
async def download_config(
build_id: str = Path(..., description="Unique build identifier"),
service: BuildsService = Depends(get_builds_service)
):
"""
Download the CustomBuild config YAML for a build.

Generated from build metadata via the shared build_config module
(same shape as the YAML packed into the archive by Builder).

Args:
build_id: The unique build identifier

Returns:
YAML config file

Raises:
404: Build not found or config could not be generated
"""
result = service.get_build_config_yaml(build_id)
if not result:
raise HTTPException(
status_code=404,
detail=(
f"Config not available for build '{build_id}'. "
"Build may not exist or metadata is incomplete."
)
)
yaml_text, filename = result
return Response(
content=yaml_text,
media_type="application/yaml",
headers={
"Content-Disposition": f'attachment; filename="{filename}"'
},
)
2 changes: 1 addition & 1 deletion web/api/v1/router.py → backend/api/v1/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"""
from fastapi import APIRouter

from web.api.v1 import vehicles, builds, admin
from backend.api.v1 import vehicles, builds, admin

# Create the main v1 router
router = APIRouter(prefix="/v1")
Expand Down
4 changes: 2 additions & 2 deletions web/api/v1/vehicles.py → backend/api/v1/vehicles.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Path

from web.schemas import (
from backend.schemas import (
VehicleBase,
VersionOut,
BoardOut,
StandardArtifactOut,
FeatureOut,
)
from web.services.vehicles import get_vehicles_service, VehiclesService
from backend.services.vehicles import get_vehicles_service, VehiclesService

router = APIRouter(prefix="/vehicles", tags=["vehicles"])

Expand Down
4 changes: 2 additions & 2 deletions web/core/__init__.py → backend/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""
Core application components.
"""
from web.core.config import get_settings
from web.core.startup import initialize_application
from backend.core.config import get_settings
from backend.core.startup import initialize_application

__all__ = [
"get_settings",
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion web/core/limiter.py → backend/core/limiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from slowapi.errors import RateLimitExceeded
from slowapi import Limiter
from slowapi.util import get_remote_address
from web.core.config import get_settings
from backend.core.config import get_settings

logger = logging.getLogger(__name__)

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
17 changes: 9 additions & 8 deletions web/main.py → backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware

from web.api.v1 import router as v1_router
from web.ui import router as ui_router
from backend.api.v1 import router as v1_router
from backend.ui import router as ui_router

from web.core.config import get_settings
from web.core.startup import initialize_application
from web.core.logging_config import setup_logging
from web.core.limiter import limiter, rate_limit_exceeded_handler
from backend.core.config import get_settings
from backend.core.startup import initialize_application
from backend.core.logging_config import setup_logging
from backend.core.limiter import limiter, rate_limit_exceeded_handler

import ap_git
import build_manager
Expand Down Expand Up @@ -134,6 +134,7 @@ async def lifespan(app: FastAPI):
title="CustomBuild API",
description="API for ArduPilot Custom Firmware Builder",
version="1.0.0",
openapi_url="/api/openapi.json",
docs_url="/api/docs",
redoc_url="/api/redoc",
lifespan=lifespan,
Expand Down Expand Up @@ -169,8 +170,8 @@ async def health_check():
parser.add_argument(
"--port",
type=int,
default=int(os.getenv("WEB_PORT", 8080)),
help="Port to run the server on (default: 8080 or WEB_PORT env var)"
default=int(os.getenv("BACKEND_PORT", 8080)),
help="Port to run the server on (default: 8080 or BACKEND_PORT env var)"
)
args = parser.parse_args()

Expand Down
1 change: 1 addition & 0 deletions web/requirements.txt → backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ packaging==25.0
jinja2==3.1.2
python-multipart==0.0.6
slowapi==0.1.9
PyYAML==6.0.2
6 changes: 3 additions & 3 deletions web/schemas/__init__.py → backend/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
"""

# Admin schemas
from web.schemas.admin import (
from backend.schemas.admin import (
RefreshVersionsResponse,
)

# Build schemas
from web.schemas.builds import (
from backend.schemas.builds import (
BuildVersionInfo,
RemoteInfo,
BuildProgress,
Expand All @@ -21,7 +21,7 @@
)

# Vehicle schemas
from web.schemas.vehicles import (
from backend.schemas.vehicles import (
VehicleBase,
VersionBase,
VersionOut,
Expand Down
File renamed without changes.
8 changes: 6 additions & 2 deletions web/schemas/builds.py → backend/schemas/builds.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import List, Literal
from typing import List, Literal, Optional

from pydantic import BaseModel, Field
from web.schemas.vehicles import VehicleBase, BoardBase, RemoteInfo
from backend.schemas.vehicles import VehicleBase, BoardBase, RemoteInfo


# --- Build Progress ---
Expand Down Expand Up @@ -47,6 +47,10 @@ class BuildSubmitResponse(BaseModel):
class BuildVersionInfo(BaseModel):
"""Version information for a build."""
id: str = Field(..., description="Version ID used for this build")
name: Optional[str] = Field(None, description="Version display name")
type: Optional[Literal["beta", "stable", "latest", "tag"]] = Field(
None, description="Version type classification"
)
remote_info: RemoteInfo = Field(
..., description="Source repository information"
)
Expand Down
File renamed without changes.
15 changes: 15 additions & 0 deletions backend/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
Business logic services for the application.
"""
from backend.services.vehicles import get_vehicles_service, VehiclesService
from backend.services.builds import get_builds_service, BuildsService
from backend.services.admin import get_admin_service, AdminService

__all__ = [
"get_vehicles_service",
"VehiclesService",
"get_builds_service",
"BuildsService",
"get_admin_service",
"AdminService",
]
Loading
Loading