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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.1.0.alpha.2 - 2026-08-27

- Stream clean, Docker-style build steps and command output through Dagger Ruby.
- Run Bundler with the available CPUs unless `BUNDLE_JOBS` is explicitly configured.
- Keep Rails artifacts identical across cold and restored dependency caches.

## 0.1.0.alpha.1 - 2026-08-27

- Reintroduce BoringBuilder as a Ruby gem backed by Dagger Ruby.
Expand Down
4 changes: 3 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ require "bundler/gem_tasks"
require "minitest/test_task"
require "rubocop/rake_task"

Minitest::TestTask.create
Minitest::TestTask.create do |task|
task.framework = 'require_relative "./test/test_helper"'
end
RuboCop::RakeTask.new

desc "Compile every Ruby source file"
Expand Down
2 changes: 1 addition & 1 deletion boringbuilder.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ Gem::Specification.new do |spec|
spec.executables = ["boringbuilder"]
spec.require_paths = ["lib"]

spec.add_dependency "dagger_ruby", "~> 0.10"
spec.add_dependency "dagger_ruby", "~> 0.11"
end
3 changes: 2 additions & 1 deletion lib/boringbuilder.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# frozen_string_literal: true

require "dagger_ruby"

require_relative "boringbuilder/version"
require_relative "boringbuilder/errors"
require_relative "boringbuilder/artifact"
require_relative "boringbuilder/configuration"
require_relative "boringbuilder/config_file"
require_relative "boringbuilder/initializer"
require_relative "boringbuilder/build_progress"
require_relative "boringbuilder/boring_cache"
require_relative "boringbuilder/mise"
require_relative "boringbuilder/pipeline"
Expand Down
90 changes: 0 additions & 90 deletions lib/boringbuilder/build_progress.rb

This file was deleted.

2 changes: 1 addition & 1 deletion lib/boringbuilder/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module BoringBuilder
class Builder
attr_reader :configuration, :progress

def initialize(configuration, progress: BuildProgress.silent)
def initialize(configuration, progress: DaggerRuby::Progress.silent)
@configuration = configuration
@progress = progress
end
Expand Down
22 changes: 12 additions & 10 deletions lib/boringbuilder/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def root_help
end

def build
options = {}
options = { progress: "pretty" }
config_file = :auto
dry_run = false
json = false
Expand All @@ -71,15 +71,18 @@ def build
return 0 if help_requested
raise OptionParser::InvalidArgument, "expected at most one PROJECT directory" if @argv.length > 1

options[:progress] = nil if json

configuration = BoringBuilder.configuration(root: @argv.first || Dir.pwd, config: config_file, **options)
project = Project.new(configuration).validate!
if dry_run
@out.puts JSON.pretty_generate(project.plan)
return 0
end

print_build_start(project, json: json)
result = @builder_class.new(configuration, progress: build_progress(configuration, json: json)).build
progress = build_progress(configuration, json: json)
print_build_start(project, json: json, progress: progress)
result = @builder_class.new(configuration, progress: progress).build
json ? @out.puts(JSON.generate(result.to_h)) : print_result(result)
0
end
Expand Down Expand Up @@ -109,7 +112,8 @@ def add_runtime_options(parser, options)
parser.on("--platform PLATFORM", "Target platform, for example linux/amd64") do |value|
options[:platform] = value
end
parser.on("--progress MODE", %w[auto plain tty], "Dagger progress: auto, plain, or tty") do |value|
parser.on("--progress MODE", %w[pretty auto plain tty dots logs],
"Build progress: pretty (default), auto, plain, tty, dots, or logs") do |value|
options[:progress] = value
end
end
Expand Down Expand Up @@ -209,26 +213,24 @@ def print_result(result)
@out.puts "Artifact: #{result.artifact_id} (#{result.artifact_name})" if result.artifact_published?
end

def print_build_start(project, json:)
def print_build_start(project, json:, progress:)
return if json || !dagger_session?

cache = BoringCache.new(project, nil, environment: @environment).mode
pipeline = project.custom_pipeline? ? "custom pipeline" : project.framework.to_s
details = [pipeline, "#{project.configuration.runtime} runtime", "#{cache} cache",
"#{project.configuration.format.to_s.tr('_', '.')} output"]

@out.puts "Building #{project.app_name} with Dagger"
@out.puts " #{details.join(' · ')}"
progress.message("Building #{project.app_name} with Dagger\n #{details.join(' · ')}")
end

def dagger_session?
@environment["DAGGER_SESSION_PORT"] && @environment["DAGGER_SESSION_TOKEN"]
end

def build_progress(configuration, json:)
enabled = !json && dagger_session? && configuration.progress.nil?
animated = enabled && @out.respond_to?(:tty?) && @out.tty?
BuildProgress.new(out: @out, enabled: enabled, animated: animated)
config = DaggerRuby::Config.new(progress: configuration.progress, verify_version: false)
config.progress_reporter(out: @out, enabled: !json && dagger_session?, environment: @environment)
end
end
end
55 changes: 43 additions & 12 deletions lib/boringbuilder/exporter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Exporter
attr_reader :project, :client, :container, :runtime, :exporters, :progress

def initialize(project, client, container, runtime:, command_runner: nil, exporters: nil,
progress: BuildProgress.silent)
progress: DaggerRuby::Progress.silent)
@project = project
@client = client
@container = container
Expand Down Expand Up @@ -56,33 +56,36 @@ def merge_receipts(current, received)
end

def export_step_name(adapter)
return "Publish artifact to BoringCache" if adapter.name == :boringcache
return "[export] Publish artifact to BoringCache" if adapter.name == :boringcache

"Export #{configuration.format.to_s.tr('_', '.')} artifact"
"[export] Write #{configuration.format.to_s.tr('_', '.')} artifact"
end

def build_asset
filename = project.output_path.basename.to_s
case configuration.format
when :directory
Exporters::Asset.new(kind: :directory, source: artifact_directory, filename: filename)
Exporters::Asset.new(kind: :directory, source: prepared_artifact_directory, filename: filename)
when :tar
Exporters::Asset.new(kind: :file, source: archive_file("artifact.tar", compression: nil), filename: filename)
when :tar_zst
Exporters::Asset.new(kind: :file, source: archive_file("artifact.tar.zst", compression: :zstd),
filename: filename)
when :oci, :docker
Exporters::Asset.new(kind: :file, source: container.as_tarball(media_types: media_types), filename: filename)
source = progress.step("[export] Create #{configuration.format.to_s.upcase} image archive") do
container.as_tarball(media_types: media_types).sync
end
Exporters::Asset.new(kind: :file, source: source, filename: filename)
end
end

def export_reference
if configuration.publish
progress.step("Publish container image") do
progress.step("[export] Publish container image") do
container.publish(configuration.publish, media_types: media_types)
end
elsif configuration.load
progress.step("Load container image") { load_image }
progress.step("[export] Load container image") { load_image }
end
end

Expand Down Expand Up @@ -123,20 +126,48 @@ def artifact_directory
@artifact_directory ||= project.artifact.export_directory(client, container)
end

def prepared_artifact_directory
@prepared_artifact_directory ||= progress.step("[export] Assemble artifact filesystem") do
artifact_directory.sync
end
end

def archive_file(filename, compression:)
packer = client.container.from(PACKER_IMAGE).with_exec(%w[apk add --no-cache tar zstd])
packer = packer.with_mounted_directory("/artifact", artifact_directory)
packer = progress.step("[export] FROM #{PACKER_IMAGE}") { client.container.from(PACKER_IMAGE).sync }
packer = run_archive_step(packer, %w[apk add --no-cache tar zstd], "[export] RUN apk add tar zstd")
packer = packer.with_mounted_directory("/artifact", prepared_artifact_directory)
tar_path = compression == :zstd ? "/artifact.tar" : "/#{filename}"
packer = packer.with_exec(
packer = run_archive_step(
packer,
[
"tar", "--sort=name", "--mtime=@0", "--owner=0", "--group=0", "--numeric-owner",
"--pax-option=delete=atime,delete=ctime", "-C", "/artifact", "-cf", tar_path, "."
]
],
"[export] RUN tar -cf #{tar_path} ."
)
packer = packer.with_exec(["zstd", "-q", "-T1", tar_path, "-o", "/#{filename}"]) if compression == :zstd
if compression == :zstd
packer = run_archive_step(
packer,
["zstd", "-q", "-T0", tar_path, "-o", "/#{filename}"],
"[export] RUN zstd -T0 #{tar_path}"
)
end
packer.file("/#{filename}")
end

def run_archive_step(container, command, name)
executed = container.with_exec(command)
progress.step(name) do
output = command_output(executed)
progress.write(output) unless progress.streaming?
executed.sync
end
end

def command_output(container)
[container.stdout, container.stderr].map(&:strip).reject(&:empty?).join("\n")
end

def media_types
configuration.format == :docker ? :DockerMediaTypes : :OCIMediaTypes
end
Expand Down
2 changes: 1 addition & 1 deletion lib/boringbuilder/mise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def install(container, tools:, workdir:, only: nil)
at: CACHE_PATH,
entry: "mise",
workdir: workdir,
name: "Prepare Mise toolchain"
name: "[build] RUN #{command.join(' ')}"
)
end

Expand Down
7 changes: 5 additions & 2 deletions lib/boringbuilder/pipeline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module BoringBuilder
class Pipeline
attr_reader :project, :client, :progress

def initialize(project, client, environment: ENV, progress: BuildProgress.silent)
def initialize(project, client, environment: ENV, progress: DaggerRuby::Progress.silent)
@project = project
@client = client
@environment = environment
Expand Down Expand Up @@ -124,7 +124,10 @@ def sync_step(container, name, output: nil)
return container if container.equal?(@last_synced_container)

@last_synced_container = progress.step(name) do
progress.write { output.call } if output
if output
result = output.call
progress.write(result) unless progress.streaming?
end
container.sync
end
end
Expand Down
3 changes: 2 additions & 1 deletion lib/boringbuilder/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Project
DEFAULT_EXCLUDES = %w[
.env
.env.*
.bundle/cache
.git
coverage
dist
Expand All @@ -29,7 +30,7 @@ def validate!
self
end

def container(client, progress: BuildProgress.silent)
def container(client, progress: DaggerRuby::Progress.silent)
return Pipeline.new(self, client, progress: progress).build(&configuration.pipeline) if custom_pipeline?

builder = rails? ? RailsBuild : RubyBuild
Expand Down
Loading