A Cloud Foundry buildpack consists of three main executable scripts that handle the application lifecycle: detect, compile, and release.
rust-buildpack/
├── bin/
│ ├── detect
│ ├── compile
│ └── release
├── lib/
│ └── common.sh
└── README.md
This script determines if the buildpack should be used for an application.
#!/usr/bin/env bash
# bin/detect
BUILD_DIR=$1
# Check for Cargo.toml (primary indicator of a Rust project)
if [ -f "$BUILD_DIR/Cargo.toml" ]; then
echo "Rust"
exit 0
fi
# Check for main.rs or lib.rs as secondary indicators
if [ -f "$BUILD_DIR/src/main.rs" ] || [ -f "$BUILD_DIR/src/lib.rs" ]; then
echo "Rust"
exit 0
fi
# Not a Rust application
exit 1This is the main script that installs Rust and compiles the application.
#!/usr/bin/env bash
# bin/compile
set -eo pipefail
BUILD_DIR=$1
CACHE_DIR=$2
ENV_DIR=$3
BUILDPACK_DIR=$(cd $(dirname $0); cd ..; pwd)
# Load common functions
source $BUILDPACK_DIR/lib/common.sh
# Determine Rust version to use
if [ -n "$RUST_VERSION" ]; then
echo " Using RUST_VERSION environment variable: $RUST_VERSION"
else
RUST_VERSION=$(get_rust_version "$BUILD_DIR")
echo " Detected Rust version: $RUST_VERSION"
fi
# Configuration - Default to stable channel
RUST_VERSION=${RUST_VERSION:-"stable"}
echo "-----> Installing Rust $RUST_VERSION"
# Create cache directory for Rust installation
RUST_CACHE_DIR="$CACHE_DIR/rust"
mkdir -p "$RUST_CACHE_DIR"
# Install Rust if not cached or cache is old
CACHE_AGE_DAYS=7 # Refresh cache weekly
if [ ! -d "$RUST_CACHE_DIR/bin" ] || [ $(find "$RUST_CACHE_DIR" -mtime +$CACHE_AGE_DAYS | wc -l) -gt 0 ]; then
echo " Downloading and installing Rust..."
# Remove old cache if it exists
rm -rf "$RUST_CACHE_DIR"
mkdir -p "$RUST_CACHE_DIR"
# Download rustup installer
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rustup-init.sh
chmod +x /tmp/rustup-init.sh
# Install Rust to cache directory
RUSTUP_HOME="$RUST_CACHE_DIR" CARGO_HOME="$RUST_CACHE_DIR" \
/tmp/rustup-init.sh -y --default-toolchain $RUST_VERSION --no-modify-path
rm /tmp/rustup-init.sh
else
echo " Using cached Rust installation"
# Update to latest stable if using stable channel
if [ "$RUST_VERSION" = "stable" ]; then
echo " Updating stable toolchain..."
RUSTUP_HOME="$RUST_CACHE_DIR" CARGO_HOME="$RUST_CACHE_DIR" \
"$RUST_CACHE_DIR/bin/rustup" update stable
fi
fi
# Set up Rust environment
export RUSTUP_HOME="$RUST_CACHE_DIR"
export CARGO_HOME="$RUST_CACHE_DIR"
export PATH="$RUST_CACHE_DIR/bin:$PATH"
# Copy Rust installation to build directory
echo "-----> Setting up Rust environment"
cp -r "$RUST_CACHE_DIR" "$BUILD_DIR/.rust"
# Set up Cargo cache
CARGO_CACHE_DIR="$CACHE_DIR/cargo"
mkdir -p "$CARGO_CACHE_DIR"
# Link cargo cache to build directory
mkdir -p "$BUILD_DIR/.cargo"
if [ -d "$CARGO_CACHE_DIR/registry" ]; then
ln -sf "$CARGO_CACHE_DIR/registry" "$BUILD_DIR/.cargo/registry"
fi
if [ -d "$CARGO_CACHE_DIR/git" ]; then
ln -sf "$CARGO_CACHE_DIR/git" "$BUILD_DIR/.cargo/git"
fi
# Set environment for compilation
export CARGO_HOME="$BUILD_DIR/.cargo"
export RUSTUP_HOME="$BUILD_DIR/.rust"
export PATH="$BUILD_DIR/.rust/bin:$PATH"
echo "-----> Building Rust application"
cd "$BUILD_DIR"
# Check if this is a web application (has dependencies like actix-web, warp, etc.)
if grep -q -E "(actix-web|warp|rocket|axum|tide)" Cargo.toml; then
echo " Detected web framework"
WEB_APP=true
else
WEB_APP=false
fi
# Build the application in release mode
cargo build --release
# Copy cargo cache back for next build
cp -r "$BUILD_DIR/.cargo/registry" "$CARGO_CACHE_DIR/" 2>/dev/null || true
cp -r "$BUILD_DIR/.cargo/git" "$CARGO_CACHE_DIR/" 2>/dev/null || true
# Create start script
echo "-----> Creating startup script"
cat > "$BUILD_DIR/start.sh" << 'EOF'
#!/usr/bin/env bash
export RUSTUP_HOME="/app/.rust"
export CARGO_HOME="/app/.cargo"
export PATH="/app/.rust/bin:$PATH"
# Find the binary to run
BINARY_NAME=$(find target/release -maxdepth 1 -type f -executable ! -name "*.so" ! -name "*.d" | head -1)
if [ -z "$BINARY_NAME" ]; then
echo "Error: No executable binary found in target/release"
exit 1
fi
echo "Starting $(basename $BINARY_NAME)..."
exec "$BINARY_NAME" "$@"
EOF
chmod +x "$BUILD_DIR/start.sh"
echo "-----> Rust buildpack compilation complete"This script provides metadata about how to run the application.
#!/usr/bin/env bash
# bin/release
BUILD_DIR=$1
# Check if this appears to be a web application
if [ -f "$BUILD_DIR/Cargo.toml" ] && grep -q -E "(actix-web|warp|rocket|axum|tide)" "$BUILD_DIR/Cargo.toml"; then
# Web application - default to PORT environment variable
cat << EOF
---
default_process_types:
web: ./start.sh
config_vars:
RUST_LOG: info
EOF
else
# CLI application
cat << EOF
---
default_process_types:
console: ./start.sh
config_vars:
RUST_LOG: info
EOF
fiShared utilities for the buildpack scripts.
#!/usr/bin/env bash
# lib/common.sh
# Output formatting functions
function puts-step() {
echo "-----> $@"
}
function puts-warn() {
echo " ! $@"
}
function puts-verbose() {
if [ -n "$VERBOSE" ]; then
echo " $@"
fi
}
# Utility to get Rust version from Cargo.toml or rust-toolchain
function get_rust_version() {
local build_dir=$1
local version="stable"
# Check for rust-toolchain.toml (newer format)
if [ -f "$build_dir/rust-toolchain.toml" ]; then
if command -v toml >/dev/null 2>&1; then
version=$(toml get "$build_dir/rust-toolchain.toml" toolchain.channel 2>/dev/null || echo "stable")
else
# Fallback parsing without toml tool
version=$(grep -E '^\s*channel\s*=' "$build_dir/rust-toolchain.toml" | sed -E 's/.*=\s*"([^"]+)".*/\1/' || echo "stable")
fi
# Check for rust-toolchain file (legacy format)
elif [ -f "$build_dir/rust-toolchain" ]; then
version=$(cat "$build_dir/rust-toolchain" | tr -d '\n\r' | head -1)
# Check Cargo.toml for rust-version (MSRV)
elif [ -f "$build_dir/Cargo.toml" ] && grep -q "rust-version" "$build_dir/Cargo.toml"; then
version=$(grep "rust-version" "$build_dir/Cargo.toml" | sed -E 's/.*=\s*"([^"]+)".*/\1/' | head -1)
echo " Found minimum Rust version $version in Cargo.toml, using stable instead"
version="stable" # Use stable even if MSRV is specified
fi
echo "$version"
}
# Check if binary exists in PATH
function check_command() {
command -v "$1" >/dev/null 2>&1
}You can configure the buildpack through environment variables:
RUST_VERSION: Specify Rust version (default: stable)CARGO_BUILD_FLAGS: Additional flags for cargo buildRUST_LOG: Set logging level for the application
-
Package the buildpack:
tar czf rust-buildpack.tgz rust-buildpack/
-
Deploy with cf CLI:
cf push myapp -b https://github.com/yourusername/rust-buildpack.git
-
Or specify in manifest.yml:
applications: - name: rust-app buildpack: https://github.com/yourusername/rust-buildpack.git command: ./start.sh
Add support for custom build commands in bin/compile:
# Check for custom build command
if [ -f "$BUILD_DIR/.buildpack-build-cmd" ]; then
BUILD_CMD=$(cat "$BUILD_DIR/.buildpack-build-cmd")
echo "-----> Running custom build command: $BUILD_CMD"
eval "$BUILD_CMD"
else
cargo build --release
fiHandle projects with multiple binaries:
# In start.sh, allow specifying which binary to run
BINARY_NAME=${1:-$(find target/release -maxdepth 1 -type f -executable ! -name "*.so" ! -name "*.d" | head -1)}Create a simple test application:
# Cargo.toml
[package]
name = "hello-rust"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.0"
tokio = { version = "1.0", features = ["full"] }// src/main.rs
use actix_web::{web, App, HttpResponse, HttpServer, Result};
use std::env;
async fn hello() -> Result<HttpResponse> {
Ok(HttpResponse::Ok().body("Hello from Rust on Cloud Foundry!"))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string());
let port: u16 = port.parse().expect("PORT must be a number");
println!("Starting server on port {}", port);
HttpServer::new(|| {
App::new()
.route("/", web::get().to(hello))
})
.bind(("0.0.0.0", port))?
.run()
.await
}This buildpack will detect Rust applications, install the Rust toolchain, compile the application, and provide appropriate runtime configuration for Cloud Foundry deployment.