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 )