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
3 changes: 3 additions & 0 deletions config/examples/basic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ frameworks:
# xcframework_output: ../SDKs
# parallel_builds: false
# clean_before_build: true
# verbose: false # Enable verbose output (shows xcodebuild logs)
# use_formatter: true # Use xcbeautify/xcpretty for formatted output
# # Can also be: "xcbeautify" or "xcpretty" to specify

# Optional: Publishing settings
# publishing:
Expand Down
1 change: 1 addition & 0 deletions lib/xcframework_cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require_relative 'xcframework_cli/config/schema'
require_relative 'xcframework_cli/config/defaults'
require_relative 'xcframework_cli/platform/registry'
require_relative 'xcframework_cli/xcodebuild/formatter'
require_relative 'xcframework_cli/xcodebuild/wrapper'
require_relative 'xcframework_cli/builder/cleaner'
require_relative 'xcframework_cli/builder/archiver'
Expand Down
7 changes: 5 additions & 2 deletions lib/xcframework_cli/builder/archiver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,17 @@ def build_archive(platform_identifier, options = {})
)

# Execute archive command
result = Xcodebuild::Wrapper.execute_archive(
archive_options = {
project: project_path,
scheme: scheme,
destination: platform.destination,
archive_path: archive_path,
build_settings: build_settings,
derived_data_path: derived_data_path
)
}
archive_options[:use_formatter] = options[:use_formatter] if options.key?(:use_formatter)

result = Xcodebuild::Wrapper.execute_archive(archive_options)

if result.success?
# Clean private Swift interfaces
Expand Down
1 change: 1 addition & 0 deletions lib/xcframework_cli/builder/orchestrator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def build_platform_archives

build_options = {}
build_options[:deployment_target] = config[:deployment_target] if config[:deployment_target]
build_options[:use_formatter] = config[:use_formatter] if config.key?(:use_formatter)

archiver.build_archives(config[:platforms], build_options)
end
Expand Down
21 changes: 15 additions & 6 deletions lib/xcframework_cli/cli/commands/build.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ module Commands
module Build
class << self
def execute(options)
setup_logger(options)
# Load configuration first to get verbose setting
config = load_configuration(options)
# Setup logger with config settings (CLI options override)
setup_logger(options, config[:_raw_config])
validate_configuration(config)
run_build(config)
rescue XCFrameworkCLI::Error => e
Expand All @@ -29,9 +31,13 @@ def execute(options)

private

def setup_logger(options)
Utils::Logger.verbose = options[:verbose]
Utils::Logger.quiet = options[:quiet]
def setup_logger(options, config = nil)
# CLI options override config file settings
verbose = options[:verbose] || (config && config[:build]&.[](:verbose)) || false
quiet = options[:quiet] || false

Utils::Logger.verbose = verbose
Utils::Logger.quiet = quiet
end

def load_configuration(options)
Expand Down Expand Up @@ -73,7 +79,9 @@ def load_from_config_file(options)
output_dir: output_dir,
platforms: framework[:platforms] || options[:platforms],
clean: config[:build][:clean_before_build].nil? ? options[:clean] : config[:build][:clean_before_build],
include_debug_symbols: options[:debug_symbols]
include_debug_symbols: options[:debug_symbols],
use_formatter: config[:build][:use_formatter],
_raw_config: config
}
end
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
Expand All @@ -88,7 +96,8 @@ def load_from_command_line(options)
output_dir: options[:output],
platforms: options[:platforms],
clean: options[:clean],
include_debug_symbols: options[:debug_symbols]
include_debug_symbols: options[:debug_symbols],
use_formatter: true # Default to true for command-line mode
}
end

Expand Down
4 changes: 3 additions & 1 deletion lib/xcframework_cli/config/defaults.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ module Defaults
output_dir: 'build',
xcframework_output: '../SDKs',
parallel_builds: false,
clean_before_build: true
clean_before_build: true,
verbose: false,
use_formatter: true
}.freeze

# Default deployment targets for each platform
Expand Down
2 changes: 2 additions & 0 deletions lib/xcframework_cli/config/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class Schema < Dry::Validation::Contract
optional(:xcframework_output).filled(:string)
optional(:parallel_builds).filled(:bool)
optional(:clean_before_build).filled(:bool)
optional(:verbose).filled(:bool)
optional(:use_formatter).filled
end

optional(:publishing).hash do
Expand Down
188 changes: 188 additions & 0 deletions lib/xcframework_cli/xcodebuild/formatter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# frozen_string_literal: true

require 'open3'

module XCFrameworkCLI
module Xcodebuild
# Formatter for xcodebuild output
# Detects and uses xcbeautify or xcpretty if available
class Formatter
FORMATTERS = %w[xcbeautify xcpretty].freeze

class << self
# Check which formatter is available
#
# @return [String, nil] Name of available formatter or nil
def detect_formatter
FORMATTERS.find { |formatter| command_available?(formatter) }
end

# Check if a command is available in PATH
#
# @param command [String] Command name
# @return [Boolean] true if command is available
def command_available?(command)
!`which #{command}`.strip.empty?
rescue StandardError
false
end

# Get formatter command with options
#
# @param formatter [String, nil] Formatter name ('xcbeautify', 'xcpretty', or nil)
# @return [Array<String>, nil] Formatter command with args, or nil if no formatter
def formatter_command(formatter = nil)
case formatter
when 'xcbeautify'
['xcbeautify']
when 'xcpretty'
['xcpretty', '--color']
else
nil
end
end

# Execute xcodebuild command with optional output formatting
#
# @param xcodebuild_command [Array<String>] Full xcodebuild command with arguments
# @param stream_output [Boolean] Whether to stream output in real-time
# @param use_formatter [Boolean, String] true to auto-detect, String to specify formatter, false to disable
# @return [Hash] Result hash with :success, :stdout, :stderr, :exit_code
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def execute_with_formatting(xcodebuild_command, stream_output: false, use_formatter: true)
# Determine formatter to use
formatter = if use_formatter == false
nil
elsif use_formatter.is_a?(String)
use_formatter if command_available?(use_formatter)
else
detect_formatter
end

if stream_output && formatter
execute_with_pipe_streaming(xcodebuild_command, formatter)
elsif stream_output
execute_with_streaming(xcodebuild_command)
else
execute_without_streaming(xcodebuild_command)
end
end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength

private

# Execute with formatter pipe and real-time streaming
#
# @param xcodebuild_command [Array<String>] xcodebuild command
# @param formatter [String] Formatter name
# @return [Hash] Result hash
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def execute_with_pipe_streaming(xcodebuild_command, formatter)
formatter_cmd = formatter_command(formatter)
stdout_lines = []
stderr_lines = []

Utils::Logger.debug("Using formatter: #{formatter}")

# Pipe xcodebuild through formatter
Open3.popen3(*xcodebuild_command) do |_stdin, xcode_stdout, xcode_stderr, xcode_wait_thr|
Open3.popen3(*formatter_cmd) do |formatter_stdin, formatter_stdout, _formatter_stderr, formatter_wait_thr|
# Thread to pipe xcodebuild stdout to formatter stdin
pipe_thread = Thread.new do
xcode_stdout.each_line do |line|
formatter_stdin.puts(line)
end
formatter_stdin.close
end

# Thread to capture and stream formatted output
stdout_thread = Thread.new do
formatter_stdout.each_line do |line|
puts line # Print to console
stdout_lines << line
end
end

# Thread to capture stderr
stderr_thread = Thread.new do
xcode_stderr.each_line do |line|
warn line # Print to stderr
stderr_lines << line
end
end

# Wait for all threads
pipe_thread.join
stdout_thread.join
stderr_thread.join

xcode_status = xcode_wait_thr.value
formatter_wait_thr.value # Wait for formatter to finish

{
success: xcode_status.success?,
stdout: stdout_lines.join,
stderr: stderr_lines.join,
exit_code: xcode_status.exitstatus
}
end
end
end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength

# Execute with real-time streaming (no formatter)
#
# @param command [Array<String>] Command to execute
# @return [Hash] Result hash
def execute_with_streaming(command)
stdout_lines = []
stderr_lines = []

Open3.popen3(*command) do |_stdin, stdout, stderr, wait_thr|
# Stream stdout
stdout_thread = Thread.new do
stdout.each_line do |line|
puts line
stdout_lines << line
end
end

# Stream stderr
stderr_thread = Thread.new do
stderr.each_line do |line|
warn line
stderr_lines << line
end
end

stdout_thread.join
stderr_thread.join
status = wait_thr.value

{
success: status.success?,
stdout: stdout_lines.join,
stderr: stderr_lines.join,
exit_code: status.exitstatus
}
end
end

# Execute without streaming (capture all output)
#
# @param command [Array<String>] Command to execute
# @return [Hash] Result hash
def execute_without_streaming(command)
stdout, stderr, status = Open3.capture3(*command)

{
success: status.success?,
stdout: stdout,
stderr: stderr,
exit_code: status.exitstatus
}
end
end
end
end
end
Loading
Loading