App-ArduinoBuilder
view release on metacpan or search on metacpan
lib/App/ArduinoBuilder.pm view on Meta::CPAN
use App::ArduinoBuilder::System 'find_arduino_dir', 'system_cwd', 'execute_cmd';
use Data::Section::Simple;
use File::Basename;
use File::Path 'remove_tree';
use File::Spec::Functions;
use Getopt::Long;
use List::Util 'any', 'none', 'first';
use Log::Log4perl;
use Log::Log4perl::Level;
use Log::Any::Adapter;
use Log::Any::Simple ':default';
use Parallel::TaskExecutor 'default_executor';
use Pod::Usage;
our $VERSION = '0.08';
# User agent used for the pluggable discovery and pluggable monitor tools.
our $TOOLS_USER_AGENT = "\"App::ArduinoBuilder ${VERSION}\"";
sub _string_to_log4perl_level {
my ($level) = @_;
return "FATAL" if $level =~ m/^FATAL$/i;
return "ERROR" if $level =~ m/^ERR(?:OR)?$/i;
return "WARNING" if $level =~ m/^WARN(:?ING)?$/i;
return "INFO" if $level =~ m/^INFO?$/i;
return "DEBUG" if $level =~ m/^(?:DBG|DEBUG)$/i;
return "TRACE" if $level =~ m/^(?:TRACE|FULL(:?_?(?:DBG|DEBUG))?)$/i;
fatal "Unknown log level: ${level}";
}
sub set_log_level {
my ($str_level) = @_;
my $log4perl_priority = Log::Log4perl::Level::to_priority(_string_to_log4perl_level($str_level));
Log::Log4perl->get_logger("")->level($log4perl_priority);
return;
}
sub Run {
# We initialize Log4perl here because we might need to log early during the
# initialization. But the default level or even the full config can be
# overriden later.
Log::Log4perl->init(\Data::Section::Simple::get_data_section('log4perl.conf'));
Log::Any::Adapter->set('Log4perl');
set_log_level($ENV{ARDUINO_BUILDER_LOG_LEVEL}) if exists $ENV{ARDUINO_BUILDER_LOG_LEVEL};
my $config = App::ArduinoBuilder::Config->new();
my (@skip, @force, @only);
GetOptions(
'help|h' => sub { pod2usage(-exitval => 0, -verbose => 2)},
'project-dir|project|p=s' => sub { $config->set('builder.project_dir' => $_[1], allow_override => 1) },
'build-dir|build|b=s' => sub { $config->set('builder.internal.build_dir' => $_[1], allow_override => 1) },
'log-level|l=s' => sub { set_log_level($_[1]) },
'config|c=s%' => sub { $config->set($_[1] => $_[2], allow_override => 1) },
'menu=s%' => sub { $config->set('builder.menu.'.$_[1] => $_[2], allow_override => 1) },
'skip=s@' => sub { push @skip, split /,/, $_[1] }, # skip this step
'force=s@' => sub { push @force, split /,/, $_[1] }, # even if it would be skipped by the dependency checker
'only=s@' => sub { push @only, split /,/, $_[1] }, # run only these steps (skip all others)
'stack-trace-on-error|stack' => sub { Log::Any::Simple::die_with_stack_trace('long') },
'parallelize|j=i' => sub { $config->set('builder.parallelize' => $_[1], allow_override => 1) },
'target-port|port=s' => sub { $config->append('builder.upload.port' => $_[1], ',')},
'force-port=s' => sub {
my ($protocol, $address) = split(/:/, $_[1]);
$config->set('builder.forced_port.protocol' => $protocol);
$config->set('builder.forced_port.address' => $address);
$config->set('builder.forced_port.forced' => 1);
},
) or pod2usage(-exitval => 2, -verbose =>0);
if (my @unknown = grep { !/^(clean|build|discover|upload|monitor)$/ } @ARGV) {
fatal "Unknown command%s: %s", (@unknown > 1 ? 's' : ''), join(', ', @unknown);
}
generate_project_config($config);
push @ARGV, 'build' unless @ARGV;
trace "Executing the following command: %s", sub { join(', ', @ARGV) };
if (grep { /^clean$/ } @ARGV) {
clean($config);
}
if (grep { /^build$/ } @ARGV) {
build($config, \@skip, \@force, \@only);
}
if (grep { /^discover$/ } @ARGV and not grep { /^(upload|monitor)$/ } @ARGV) {
discover($config);
}
if (grep { /^upload$/ } @ARGV) {
discover($config);
upload($config);
}
if (grep { /^monitor$/ } @ARGV) {
# The upload process potentially modifies the board port, so we run the
# discovery here even if it was run on upload.
discover($config);
monitor($config);
}
}
sub generate_project_config {
my ($config) = @_;
my $project_dir_is_cwd = 0;
my $project_dir;
if (!$config->exists('builder.project_dir')) {
$project_dir_is_cwd = 1;
$project_dir = system_cwd();
$config->set('builder.project_dir' => $project_dir);
debug 'Using the current directory as the project dir: %s', $project_dir;
} else {
$project_dir = $config->get('builder.project_dir');
debug 'Using the specified project dir: %s', $project_dir;
}
$config->read_file(catfile($project_dir, 'arduino_builder.local'), allow_missing => 1);
$config->read_file(catfile($project_dir, 'arduino_builder.config'), allow_missing => 1);
my $build_dir;
if (!$config->exists('builder.internal.build_dir')) {
if ($config->exists('builder.default_build_dir')) {
$build_dir = $config->get('builder.default_build_dir');
$config->set('builder.internal.build_dir_from_default' => 1);
debug 'Using the default build dir: %s', $build_dir;
} elsif (!$project_dir_is_cwd) {
$build_dir = system_cwd();
debug 'Using the current directory as the build dir: %s', $build_dir;
} else {
fatal 'No builder.default_build_dir config and --build_dir was not passed when building from the project directory.';
}
$config->set('builder.internal.build_dir' => $build_dir);
} else {
debug 'Using the explicitly specified build dir: %s', $config->get('builder.internal.build_dir')
}
$config->set('build.path' => $config->get('builder.internal.build_dir'));
if (!$config->exists('builder.source.path')) {
my $d = first { -d catdir($project_dir, $_) } qw(src srcs source sources);
if (defined $d) {
$config->set('builder.source.path' => catdir($project_dir, $d));
$config->set('builder.source.is_recursive' => 1, ignore_existing => 1);
debug 'Using the following directory as the source dir (recursively): %s', $d;
} else {
$config->set('builder.source.path' => $project_dir);
$config->set('builder.source.is_recursive' => 0, ignore_existing => 1);
debug 'Using the project dir (non-recursively) as the source dir.';
}
} else {
$config->set('builder.source.is_recursive' => 1, ignore_existing => 1);
debug 'Using an explicitly specified source dir (%s): %s', ($config->get('builder.source.is_recursive') ? 'recursively' : 'non-recursively'), $config->get('builder.source.path');
}
my $arduino_dir;
if ($config->exists('builder.arduino.install_dir')) {
$arduino_dir = $config->get('builder.arduino.install_dir');
lib/App/ArduinoBuilder.pm view on Meta::CPAN
# build only the code inside the src/ directory
my $built_sketch = $builder->build_object_files(
$config->get('builder.source.path'), catdir($config->get('build.path'), 'sketch'),
[], $force->('sketch'), !$config->get('builder.source.is_recursive'));
info ($built_sketch ? ' Success' : ' Already up-to-date');
$built_something |= $built_sketch;
$builder->run_hook('sketch.postbuild');
}
# Bug: there is a similar bug to the one in build_archive: if a source file is
# removed, we wonât remove itâs object file. I guess we could try to detect it.
# Meanwhile itâs probably acceptable to ask for a cleanup from time to time.
my @object_files = find_all_files_with_extensions(catdir($config->get('build.path'), 'sketch'), ['o']);
for my $l (@all_libs) {
push @object_files, find_all_files_with_extensions(catdir($config->get('build.path'), 'libs', $l), ['o']);
}
debug 'Object files: '.join(', ', @object_files);
info 'Linking binary...';
if (($built_something && $run_step->('link')) || $force->('link')) {
$built_something = 1;
$builder->run_hook('linking.prelink');
$builder->link_executable(\@object_files, 'core.a');
$builder->run_hook('linking.postlink');
info ' Success';
} else {
info ' Already up-to-date';
}
info 'Extracting binary data';
if (($built_something && $run_step->('objcopy')) || $force->('objcopy')) {
$builder->run_hook('objcopy.preobjcopy');
$builder->objcopy();
$builder->run_hook('objcopy.postobjcopy');
info ' Success';
} else {
info ' Already up-to-date';
}
info 'Computing binary sketch size';
if (($built_something && $run_step->('size')) || $force->('size')) {
$builder->compute_binary_size();
# Not printing 'Success' here because the command already has an output.
} else {
info ' No new binary built';
}
info 'Success!';
}
sub discover {
my ($config) = @_;
if ($config->get('builder.forced_port.forced', default => 0)) {
info 'Skipping board discovery';
return;
}
info 'Running board discovery...';
my @ports = App::ArduinoBuilder::Discovery::discover($config);
# Discovery can be run more than once, as the port of a board can be changed
# after upload. So we override any previous discovered ports.
$config->set('builder.internal.ports' => \@ports, allow_override =>1);
if (@ports) {
debug 'Found port%s: %s', (@ports > 1 ? 's' : ''), join(', ', map { $_->get('upload.port.label') } @ports);
} else {
warning 'No port found.';
}
}
sub select_port {
my ($config) = @_;
if ($config->get('builder.forced_port.forced', default => 0)) {
# A forced port does not match a found port (as there are none), so we canât
# just set selected_port here.
return $config->filter('builder.forced_port')->prefix('upload.port');
}
{
my $port = $config->get('builder.internal.selected_port', default => undef);
if (defined $port) {
debug 'Using previously selected port: %s', $port->get('upload.port.label');
return $port;
}
}
my @ports = @{$config->get('builder.internal.ports')};
# TODO: implement an exact match selection and an interactive selection.
fatal "You must pass the --target-port option to select the upload target" unless $config->exists('builder.upload.port');
my @targets = map { qr/^$_$/i } split(/\s*,\s*/, $config->get('builder.upload.port'));
@ports = grep { my $port = $_; any { $port->get('upload.port.lc_label') =~ m/$_/ || $port->get('upload.port.lc_address') =~ m/$_/ } @targets } @ports;
unless (@ports) {
fatal "None of the specified ports (%s) can be found, can your target be found by the 'discover' command?", join(', ', @targets);
}
warn "More than one found port match with builder.upload.port. Picking the firt one." if @ports > 1;
my $port = $ports[0];
info 'Using the first match port from the configuration: %s', $port->get('upload.port.address');
$config->set('builder.internal.selected_port' => $port);
return $port;
}
sub upload {
my ($config) = @_;
info 'Uploading binary to the board...';
my $port = select_port($config);
my $protocol = $port->get('upload.port.protocol');
my $tool = $config->get("upload.tool.${protocol}", default => $config->get('upload.tool.default', default => $config->get('upload.tool')));
my $tool_config = $config->filter("tools.${tool}");
# TODO: add a way to set the verbose mode, in which case the upload.params.verbose
# property should be copied, instead of upload.params.quiet.
# Reference: https://arduino.github.io/arduino-cli/0.32/platform-specification/#verbose-parameter
$tool_config->set('upload.verbose' => $tool_config->get('upload.params.quiet'), allow_override => 1);
my $upload_config = $config->filter("upload.${protocol}")->prefix('upload');
$upload_config->merge($tool_config);
$upload_config->merge($port);
# Note: $config is in the recursive base in $upload_config.
# TODO: Before executing the command, some boards require that we manually
# reset them through their Serial port.
# See the code: https://github.com/arduino/arduino-cli/blob/ad9ddb882016c2af10e0db3785a46122bc9cfb1f/commands/upload/upload.go#L370
# And the doc: https://arduino.github.io/arduino-cli/0.32/platform-specification/#1200-bps-bootloader-reset
my $cmd = $upload_config->get('upload.pattern');
debug "Upload configuration:\n%s", sub { $upload_config->dump(' ') };
default_executor()->run_now(sub {
close STDIN;
execute_cmd($cmd);
});
info 'Success!';
}
sub monitor {
my ($config) = @_;
info 'Running board monitor. Press ctrl+c to exit...';
my $port = select_port($config);
App::ArduinoBuilder::Monitor::monitor($config, $port);
info 'Success!';
}
1;
__DATA__
@@ log4perl.conf
# We send all messages to two appenders (one that prints the level and the
# other that does not). But they have each a LevelMatch filter attached so
# that each message is eventually only printed once.
# This is so that frequent message (below the INFO level) are printed without
# their level to increase readability (especially for command lines). More
# serious messages are displayed with their level.
#
# All of that can be configured with the --logconf flag.
#
# Remember that we have a custom log level.
#
# The default log level itself is set to a default here but can be configured
# later too.
log4perl.rootLogger = INFO, ScreenWithLevel, ScreenWithoutLevel
# Configuration of the ScreenWithLevel appender for the levels INFO to
# FATAL.
log4perl.appender.ScreenWithLevel = Log::Log4perl::Appender::Screen
log4perl.appender.ScreenWithLevel.layout = PatternLayout
log4perl.appender.ScreenWithLevel.layout.ConversionPattern = %p: %m%n
log4perl.appender.ScreenWithLevel.Filter = InfoToFatalFilter
log4perl.filter.InfoToFatalFilter = Log::Log4perl::Filter::LevelRange
log4perl.filter.InfoToFatalFilter.LevelMin = INFO
log4perl.filter.InfoToFatalFilter.LevelMax = FATAL
log4perl.filter.InfoToFatalFilter.AcceptOnMatch = true
# Configuration of the ScreenWithoutLevel appender for the levels levels below
# INFO.
log4perl.appender.ScreenWithoutLevel = Log::Log4perl::Appender::Screen
log4perl.appender.ScreenWithoutLevel.layout = PatternLayout
log4perl.appender.ScreenWithoutLevel.layout.ConversionPattern = %m%n
log4perl.appender.ScreenWithoutLevel.Filter = DebugFilter
( run in 1.519 second using v1.01-cache-2.11-cpan-b16cb0d3907 )