Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b3d1453
test: pass GITHUB_TOKEN to docker containers running the railpack bui…
iloveitaly Sep 1, 2025
3fabf6e
fix: fallback to raw version when no package versions exist (#245)
iloveitaly Sep 2, 2025
650434e
fix: support node projects without dependencies (#242)
iloveitaly Sep 2, 2025
a539e84
feat: support react router build cache (#244)
iloveitaly Sep 3, 2025
aaa32ef
fix: improved buildkit error message (#237)
iloveitaly Sep 3, 2025
cac8b8f
refactor: use consistent local layers entrypoint (#241)
iloveitaly Sep 3, 2025
9f9f0b8
fix: use default mise backends for python packages (#249)
iloveitaly Sep 3, 2025
8b5c1da
fix: --verbose enables mise verbose logging (#248)
iloveitaly Sep 3, 2025
6be63d1
docs: add mise installation instructions for railpack
iloveitaly Sep 4, 2025
856f33b
Revert "fix: use default mise backends for python packages (#249)" (#…
coffee-cup Sep 5, 2025
3632834
feat: support for expanded platform arguments (#246)
iloveitaly Sep 9, 2025
9ad78e6
chore: mise update 2025.9.6 (#254)
iloveitaly Sep 9, 2025
5fb67db
fix: hard fail on invalid railpack.json (#226)
iloveitaly Sep 10, 2025
9b3d7e9
feat: add support for .bun-version file (#258)
coffee-cup Sep 11, 2025
4fcb872
test: use poetry cli to run python (#257)
iloveitaly Sep 11, 2025
7f73fb7
update mise 2025.9.9 (#262)
iloveitaly Sep 11, 2025
2f40d64
fix: remove python3-dev apt package (#256)
iloveitaly Sep 16, 2025
1bf8816
Update node examples (#266)
railway-bot Sep 16, 2025
0441e56
Update Rust examples (#268)
coffee-cup Sep 16, 2025
780c145
Update default versions for Go, Python, Ruby, and Rust providers (#267)
coffee-cup Sep 16, 2025
334f1c1
Add configurable skippable commands list to pretty print output (#271)
coffee-cup Sep 17, 2025
a26dee9
fix: gracefully handle resolution failures for packages with SkipMise…
coffee-cup Sep 19, 2025
38c5628
update mise 2025.9.17 (#274)
iloveitaly Sep 24, 2025
dc7c70a
feat: dockerignore support (#263)
iloveitaly Sep 24, 2025
186e21e
fix: missing local layer usage (#276)
iloveitaly Sep 25, 2025
05b5ba6
update mise 2025.9.18 (#275)
iloveitaly Sep 25, 2025
cd55c21
fix: use containerd platform parsing to fix platform parsing (#277)
iloveitaly Sep 26, 2025
858cb95
test: validate all of the railpack.json against the generated schema
iloveitaly Sep 27, 2025
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: 4 additions & 0 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ jobs:
- name: Install dependencies
run: go mod download

# required for integration tests that use multiple platforms
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Run test for ${{ matrix.example }}
env:
# without the GITHUB_TOKEN, mise will 403 us
Expand Down
25 changes: 11 additions & 14 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with
code in this repository.

# What is Railpack

Zero-config application builder that automatically analyzes your code and turns
it into a container image. It's the successor to Nixpacks, built on BuildKit
it into a container image. It's built on BuildKit
with support for Node, Python, Go, PHP, and more.

# Architecture
Expand All @@ -20,25 +15,27 @@ with support for Node, Python, Go, PHP, and more.
- **Providers**: Language-specific modules that detect project types (e.g. Node
detects package.json) and generate appropriate build steps

# Bash commands

- `mise run build` - Build the CLI binary
- `mise run check` - Run linting, formatting, and static analysis
- `mise run test` - Run unit tests

# Code style

- Follow Go conventions and existing patterns in the codebase
- Use appropriate error handling with proper error wrapping
- Do not write comments that are obvious from the code itself; focus on
explaining why something is done, not what it does
- Seriously, do not write comments that are obvious from the code itself.
- Do not write one-line functions
- Always use the App abstraction for file system operations.

# Workflow

- Do not use `go` directly. Instead, inspect @mise.toml and use `mise run <task>` to run various dev lifecycle commands.
- Be sure to run `mise run check` when you're done making code changes
- Don't run tests manually unless instructed to do so
- Run unit tests and a couple of relevant integration tests to verify your changes
- Don't run tests manually using `go test` unless instructed to do so
- If tests are failing that are unrelated to your changes, let me know and stop working.
- Use the `cli` mise task to test your changes on a specific example project, i.e. `mise run cli -- --verbose build --show-plan examples/node-vite-react-router-spa/`
- Do not run any write operations with `git`
- Do not use `bin/railpack` instead use `mise run cli` (which is the development build of `railpack`)

# File Conventions

- Markdown files in @docs/src/content/docs/ should be limited to 80 columns
- Markdown files in @docs/src/content/docs/ should be limited to 80 columns
38 changes: 30 additions & 8 deletions buildkit/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package buildkit

import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
Expand All @@ -10,6 +11,7 @@ import (
"time"

"github.com/charmbracelet/log"
"github.com/containerd/platforms"
"github.com/moby/buildkit/client"
_ "github.com/moby/buildkit/client/connhelper/dockercontainer"
_ "github.com/moby/buildkit/client/connhelper/nerdctlcontainer"
Expand All @@ -23,14 +25,32 @@ import (
"github.com/tonistiigi/fsutil"
)

const (
buildkitHostNotSetError = `BUILDKIT_HOST environment variable is not set.

To start a local BuildKit daemon and set the environment variable run:

docker run --rm --privileged -d --name buildkit moby/buildkit
export BUILDKIT_HOST='docker-container://buildkit'`

buildkitInfoError = `failed to get buildkit information.

Most likely the $BUILDKIT_HOST is not running. Here's an example of how to start the build container:

docker run --rm --privileged -d --name buildkit moby/buildkit

Use 'railpack --verbose' to view more error details.
`
)

type BuildWithBuildkitClientOptions struct {
ImageName string
DumpLLB bool
OutputDir string
ProgressMode string
SecretsHash string
Secrets map[string]string
Platform BuildPlatform
Platform string
ImportCache string
ExportCache string
CacheKey string
Expand All @@ -47,12 +67,12 @@ func BuildWithBuildkitClient(appDir string, plan *plan.BuildPlan, opts BuildWith

buildkitHost := os.Getenv("BUILDKIT_HOST")
if buildkitHost == "" {
log.Error("BUILDKIT_HOST environment variable is not set")
return fmt.Errorf("BUILDKIT_HOST environment variable is not set")
return errors.New(buildkitHostNotSetError)
}

log.Debugf("Connecting to buildkit host: %s", buildkitHost)

// connecting to the buildkit host does *not* mean the specified build container is running
c, err := client.New(ctx, buildkitHost)
if err != nil {
return fmt.Errorf("failed to connect to buildkit: %w", err)
Expand All @@ -62,12 +82,14 @@ func BuildWithBuildkitClient(appDir string, plan *plan.BuildPlan, opts BuildWith
// Get the buildkit info early so we can ensure we can connect to the buildkit host
info, err := c.Info(ctx)
if err != nil {
return fmt.Errorf("failed to get buildkit info: %w", err)
log.Debugf("error getting buildkit info: %v", err)
return errors.New(buildkitInfoError)
}

buildPlatform := opts.Platform
if (buildPlatform == BuildPlatform{}) {
buildPlatform = DetermineBuildPlatformFromHost()
// Parse the platform string using our helper function
buildPlatform, err := ParsePlatformWithDefaults(opts.Platform)
if err != nil {
return fmt.Errorf("failed to parse platform '%s': %w", opts.Platform, err)
}

llbState, image, err := ConvertPlanToLLB(plan, ConvertPlanOptions{
Expand Down Expand Up @@ -155,7 +177,7 @@ func BuildWithBuildkitClient(appDir string, plan *plan.BuildPlan, opts BuildWith
return fmt.Errorf("error creating FS: %w", err)
}

log.Debugf("Building image for %s with BuildKit %s", buildPlatform.String(), info.BuildkitVersion.Version)
log.Debugf("Building image for %s with BuildKit %s", platforms.Format(buildPlatform), info.BuildkitVersion.Version)

secretsMap := make(map[string][]byte)
for k, v := range opts.Secrets {
Expand Down
4 changes: 2 additions & 2 deletions buildkit/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
)

type ConvertPlanOptions struct {
BuildPlatform BuildPlatform
BuildPlatform specs.Platform

// Hash of all the secrets values that can be used to invalidate the layer cache when a secret changes
SecretsHash string
Expand All @@ -34,7 +34,7 @@ const (
)

func ConvertPlanToLLB(plan *p.BuildPlan, opts ConvertPlanOptions) (*llb.State, *Image, error) {
platform := opts.BuildPlatform.ToPlatform()
platform := opts.BuildPlatform

localState := llb.Local("context",
llb.SharedKeyHint("local"),
Expand Down
28 changes: 10 additions & 18 deletions buildkit/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/moby/buildkit/frontend/gateway/client"
gw "github.com/moby/buildkit/frontend/gateway/grpcclient"
"github.com/moby/buildkit/util/appcontext"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/railwayapp/railpack/core/plan"
)
Expand Down Expand Up @@ -120,31 +121,22 @@ func readRailpackPlan(ctx context.Context, c client.Client) (*plan.BuildPlan, er
return plan, nil
}

// validatePlatform checks if the platform is supported and returns the corresponding BuildPlatform
func validatePlatform(opts map[string]string) (BuildPlatform, error) {
// validatePlatform checks if the platform is supported and returns the corresponding specs.Platform
func validatePlatform(opts map[string]string) (specs.Platform, error) {
platformStr := opts["platform"]
if platformStr == "" {
// Default to host platform if none specified
return DetermineBuildPlatformFromHost(), nil
}

// Error if multiple platforms are specified
if strings.Contains(platformStr, ",") {
return BuildPlatform{}, fmt.Errorf("multiple platforms are not supported, got: %s", platformStr)
return specs.Platform{}, fmt.Errorf("multiple platforms are not supported, got: %s", platformStr)
}

// Match against supported platforms
switch platformStr {
case PlatformLinuxAMD64.String():
return PlatformLinuxAMD64, nil
case PlatformLinuxARM64.String():
return PlatformLinuxARM64, nil
default:
return BuildPlatform{}, fmt.Errorf("unsupported platform: %s. Must be one of: %s, %s",
platformStr,
PlatformLinuxAMD64.String(),
PlatformLinuxARM64.String())
// Parse the platform using our helper function
platform, err := ParsePlatformWithDefaults(platformStr)
if err != nil {
return specs.Platform{}, fmt.Errorf("invalid platform format: %s. Must be one of: linux/amd64, linux/arm64, etc", platformStr)
}

return platform, nil
}

// Read a file from the build context
Expand Down
66 changes: 30 additions & 36 deletions buildkit/platform.go
Original file line number Diff line number Diff line change
@@ -1,45 +1,39 @@
package buildkit

import (
"fmt"
"runtime"

"github.com/containerd/platforms"
specs "github.com/opencontainers/image-spec/specs-go/v1"
)

type BuildPlatform struct {
OS string
Architecture string
Variant string
}

var (
PlatformLinuxAMD64 = BuildPlatform{
OS: "linux",
Architecture: "amd64",
// ParsePlatformWithDefaults parses a platform string and returns the corresponding specs.Platform.
// If the input is empty, it defaults to a Linux platform that matches the host architecture.
//
// This function handles the common case where we need to map host platforms to container platforms.
// We cannot use platforms.DefaultSpec() directly because it returns the host platform
// (e.g., darwin/arm64/v8 on macOS), but container base images only support Linux platforms.
// Instead, we map the host architecture to the corresponding Linux container platform
// to provide optimal performance while ensuring compatibility with container runtimes.
//
// Examples:
// - "" -> linux/amd64 (on Intel hosts) or linux/arm64/v8 (on ARM hosts)
// - "linux/amd64" -> linux/amd64
// - "linux/arm64" -> linux/arm64
// - "linux/arm64/v8" -> linux/arm64/v8
func ParsePlatformWithDefaults(platformStr string) (specs.Platform, error) {
if platformStr == "" {
// Default to Linux platform for container builds
// We cannot use platforms.DefaultSpec() directly because it returns the host platform
// (e.g., darwin/arm64/v8 on macOS), but container base images only support Linux platforms.
// Instead, we map the host architecture to the corresponding Linux container platform
// to provide optimal performance while ensuring compatibility with container runtimes.
hostPlatform := platforms.DefaultSpec()
if hostPlatform.Architecture == "arm64" {
return specs.Platform{OS: "linux", Architecture: "arm64", Variant: "v8"}, nil
} else {
return specs.Platform{OS: "linux", Architecture: "amd64"}, nil
}
}
PlatformLinuxARM64 = BuildPlatform{
OS: "linux",
Architecture: "arm64",
Variant: "v8",
}
)

func DetermineBuildPlatformFromHost() BuildPlatform {
if runtime.GOARCH == "arm64" {
return PlatformLinuxARM64
}
return PlatformLinuxAMD64
}

func (p BuildPlatform) String() string {
return fmt.Sprintf("%s/%s", p.OS, p.Architecture)
}

func (p BuildPlatform) ToPlatform() specs.Platform {
return specs.Platform{
OS: p.OS,
Architecture: p.Architecture,
Variant: p.Variant,
}
// Parse the user-specified platform string
return platforms.Parse(platformStr)
}
Loading