Group
Extension

App-ArduinoBuilder/lib/App/ArduinoBuilder.pm

package App::ArduinoBuilder;

use 5.026;
use strict;
use warnings;
use utf8;

use App::ArduinoBuilder::Builder 'build_archive', 'build_object_files', 'link_executable', 'run_hook';
use App::ArduinoBuilder::Config 'get_os_name';
use App::ArduinoBuilder::Discovery;
use App::ArduinoBuilder::FilePath 'find_latest_revision_dir', 'list_sub_directories', 'find_all_files_with_extensions';
use App::ArduinoBuilder::Monitor;
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');
    debug 'Using the explicitly specified arduino directory: %s', $arduino_dir;
  } else {
    $arduino_dir = find_arduino_dir();
    debug 'Using the discovered arduino directory: %s', $arduino_dir;
  }

  if (!$config->exists('builder.package.path')) {
    fatal 'At least one of builder.package.path or builder.package.name must be specified in the config' unless $config->exists('builder.package.name');
    # TODO: the core package can also be installed in a "hardware" directory in
    # the sketch directory. We should search for it there.
    fatal "The builder.package.path config is not set and Arduino installation directory not found" unless $arduino_dir;
    my $package_name = $config->get('builder.package.name');
    my @tests = (catdir($arduino_dir, 'packages', $package_name), catdir($arduino_dir, 'hardware', $package_name));
    my @dirs = grep { -d } @tests;
    fatal "Cannot find the package directory for '${package_name}' inside Arduino directory: ${arduino_dir}" unless @dirs;
    debug 'Using package directory found from its name (%s): %s', $package_name, $dirs[0];
    $config->set('builder.package.path' => $dirs[0]);
  } else {
    if ($config->exists('builder.package.name')) {
      warning 'Both builder.package.path and builder.package.name were specified in the config. Only the former will be used.';
    } else {
      $config->set('builder.package.name' => basename($config->get('builder.package.path')));
    }
    debug 'Using the explicitly specified package directory: %s', $config->get('builder.package.path');
  }

  my $package_path = $config->get('builder.package.path');
  fatal "Package path does not exist: ${package_path}" unless -d $package_path;
  my $hardware_dir = catdir($package_path, 'hardware');
  $hardware_dir = $package_path unless -d $hardware_dir;
  if (!$config->exists('builder.package.arch')) {
    my @arch_dirs = list_sub_directories($hardware_dir);
    if (@arch_dirs == 1) {
      debug "Using arch '${arch_dirs[0]}'";
      $config->set('builder.package.arch' => $arch_dirs[0]);
    } else {
      fatal 'The builder.package.arch config is not set and more than one arch is present in the package: '.$hardware_dir;
    }
  }
  debug "Project config:\n%s", sub { $config->dump('  ') };

  my $hardware_path = find_latest_revision_dir(catdir($hardware_dir, $config->get('builder.package.arch')));

  my $all_boards_config = App::ArduinoBuilder::Config->new(
      files => [catfile($hardware_path, 'boards.local.txt'),
                catfile($hardware_path, 'boards.txt')],
      allow_missing => 1);
  warning "Could not find any board config file" unless $all_boards_config->nb_files();
  my $board_name = $config->get('builder.package.board');
  my $board_config = $all_boards_config->filter($board_name);
  # In $all_board_config we may have interesting values (or maybe even important
  # ones – altough not with the core I checked). In particular the name of each
  # menu. However, merging it here result in a dump that is far too large when
  # we are in full debug mode.
  #$board_config->merge($all_boards_config);

  # We should check whether a menu can be configured in the platform.txt, if so
  # this block should be moved to below (and platform.txt should be merged into
  # what is currently called board_config).
  my $menu_config = $config->filter('builder.menu');
  my $board_menu = $board_config->filter('menu');
  for my $m ($menu_config->keys()) {
    my $v = $menu_config->get($m);
    # This is one of the rare case where we want a merge to override previously
    # existing config. Although we still don’t want to override config set
    # directly by the user (e.g. on the command line), which is way all this is
    # done in a temporary config.
    my $menu_value = $board_menu->filter("${m}.${v}");
    trace "Merging menu values for menu '${m}' with key '${v}':\n%s", sub { $menu_value->dump('  ') };
    $board_config->merge($menu_value, allow_override => 1);
  }
  $config->merge($board_config);
  # TODO: warn about unset menus

  # TODO: Handles core, variant and tools references:
  # https://arduino.github.io/arduino-cli/0.32/platform-specification/#core-reference

  my @package_config_files = grep { -f } map { catfile($hardware_path, $_) } qw(platform.local.txt platform.txt programmers.local.txt programmers.txt);
  map { $config->read_file($_) } @package_config_files;

  # https://arduino.github.io/arduino-cli/0.32/platform-specification/#global-predefined-properties
  $config->set('runtime.platform.path' => $hardware_path);
  # Unclear what the runtime.hardware.path variable is supposed to point to.
  $config->set('runtime.os' => get_os_name());
  $config->set('runtime.ide.version' => '10607');
  $config->set('ide_version' => '10607');
  $config->set('software' => 'ARDUINO');
  # todo: name, _id, build.fqbn, and the time options
  $config->set('build.source.path' => $config->get('builder.source.path'));
  $config->set('sketch_path' => $config->get('builder.source.path'));
  $config->set('build.project_name' => $config->get('builder.project_name'));
  $config->set('build.arch' => uc($config->get('builder.package.arch')));  # Undocumented but it seems that it’s always upper case.
  $config->set('build.core.path', catdir($hardware_path, 'cores', $config->get('build.core')));
  $config->set('build.system.path', catdir($hardware_path, 'system'));
  $config->set('build.variant.path', catdir($hardware_path, 'variants', $config->get('build.variant')));

  my @tools_dirs = (catdir($package_path, 'tools'));
  if ($arduino_dir) {
    push @tools_dirs, catdir($arduino_dir, 'packages', 'builtin', 'tools');
    push @tools_dirs, catdir($arduino_dir, 'packages', 'arduino', 'tools');
  } else {
    warning 'The Arduino GUI directory could not be found, we won’t use the builtin tools.';
  }

  my @all_tools;
  for my $tools_dir (@tools_dirs) {
    next unless -d $tools_dir;
    my @tools = list_sub_directories($tools_dir);
    for my $t (@tools) {
      my $tool_path = catdir($tools_dir, $t);
      my $latest_tool_path = find_latest_revision_dir($tool_path);
      # That one could point to the latest version found across all packages
      # instead of the first version (possibly that of "our" package).
      $config->set("runtime.tools.${t}.path", $latest_tool_path, ignore_existing => 1);
      for my $v (list_sub_directories($tools_dir)) {
        $config->set("runtime.tools.${t}-${v}.path", catdir($tool_path, $v), ignore_existing => 1);
      }
    }
    push @all_tools, splice @tools;
  }
  debug "Found tools: %s", [sort @all_tools];

  my $config_append = $config->filter('builder.config.append');
  for my $k ($config_append->keys()) {
    $config->append($k, $config_append->get($k));
  }

  # TODO: we should create config for all the tools defined by all the other
  # platform (not overriding the existing definitions). There are some other
  # considerations that we are not handling yet from:
  # https://arduino.github.io/arduino-cli/0.32/package_index_json-specification/#how-a-tools-path-is-determined-in-platformtxt

  trace "Complete configuration: \n%s", sub { $config->dump('  ') };

  if ($config->exists('builder.parallelize')) {
    default_executor()->set_max_parallel_tasks($config->get('builder.parallelize'));
  }

  return 1;
}

sub clean {
  my ($config) = @_;

  info 'Cleaning the build directory...';

  # TODO: add a way to clean only parts of the projects.
  my $build_dir = $config->get('builder.internal.build_dir');
  if ($config->get('builder.internal.build_dir_from_default', default => 0)) {
    remove_tree($build_dir, {safe => 1, keep_root => 1});
  } else {
    warning "For safety reason we can only clean build directory specified in the project";
    warning "config. You should run `rm -rf` manually.";
    fatal "Not cleaning context dependent directory: ${build_dir}";
  }
}

sub build {
  my ($config, @array_args) = @_;
  my @skip = @{$array_args[0]};
  my @force = @{$array_args[1]};
  my @only = @{$array_args[2]};

  my $run_step = sub {
    my ($step) = @_;
    return (none { $_ eq $step } @skip) && (!@only || any { $_ eq $step } @only);
  };
  my $force = sub {
    my ($step) = @_;
    return any { $_ eq $step } @force;
  };


  my $builder = App::ArduinoBuilder::Builder->new($config);
  my $built_something = 0;

  $builder->run_hook('prebuild');

  $config->append('includes', '"-I'.$config->get('build.core.path').'"');
  $config->append('includes', '"-I'.$config->get('build.variant.path').'"');

  # This should be set to 1 first, if we start with doing library discovery at
  # some point.
  $config->set('build.library_discovery_phase' => 0);

  if ($run_step->('core')) {
    info 'Building core...';
    $builder->run_hook('core.prebuild');
    my $built_core = $builder->build_archive([$config->get('build.core.path'), $config->get('build.variant.path')], catdir($config->get('build.path'), 'core'), 'core.a', $force->('core'));
    info ($built_core ? '  Success' : '  Already up-to-date');
    $built_something ||= $built_core;
    $builder->run_hook('core.postbuild');
  }

  # Reference for all this library part:
  # https://arduino.github.io/arduino-cli/0.32/library-specification/
  # For now, we are not doing library discovery automatically. See also this:
  # https://arduino.github.io/arduino-cli/0.32/sketch-build-process/#dependency-resolution
  my $lib_config = $config->filter('builder.library');
  my @all_libs = $lib_config->keys();  # We will add config in lib_config, so let’s get the set of keys now.
  # We are first adding all the includes path for all the library before building any of them.
  for my $l ($lib_config->keys()) {
    my $lib_dir = $lib_config->get($l);
    fatal "Library directory does not exist for library '${l}': ${lib_dir}" unless -d $lib_dir;
    my $lib_properties_file = catfile($lib_dir, 'library.properties');
    my $has_lib_properties = -f $lib_properties_file;
    my $recursive_lib = $has_lib_properties && -d catdir($lib_dir, 'src');
    # These config entry (and any other added to $lib_config) are undocummented and should never be
    # set by the user.
    $lib_config->set($l.'.is_flat' => !$recursive_lib);
    if ($recursive_lib) {
      $config->append('includes', '"-I'.catdir($lib_dir, 'src').'"');
    } else {
      $config->append('includes', "\"-I${lib_dir}\"");
    }
    if ($has_lib_properties) {
      my $lib_properties = App::ArduinoBuilder::Config->new(file => $lib_properties_file);
      $lib_config->set($l.'.name' => $lib_properties->get('name', default => $l));
    }
  }
  # TODO: we are ignoring the dot_a_linkage properties of the libraries (unclear what it brings)
  # and also the precompiled
  if ($run_step->('libraries')) {
    info 'Building libraries...';
    $builder->run_hook('libraries.prebuild');
    for my $l (@all_libs) {
      # Todo: we could add "lib-$l" pseudo-steps, but we need to take care of the --only
      # interaction with the 'libraries' step.
      info '  Building library %s...', $lib_config->get("${l}.name");
      my $base_dir = $lib_config->get($l);
      my $output_dir = catdir($config->get('build.path'), 'libs', $l);
      my $built_lib;
      if ($lib_config->get("${l}.is_flat")) {
        my $utility_dir = catdir($base_dir, 'utility');
        my @dirs = ($base_dir, (-d $utility_dir ? $utility_dir : ()));
        $built_lib = $builder->build_object_files(\@dirs, $output_dir, [], $force->('libraries'), 1);
      }  else {
        $built_lib = $builder->build_object_files(catdir($base_dir, 'src'), $output_dir, [], $force->('libraries'));
      }
      info ($built_lib ? '    Success' : '    Already up-to-date');
      $built_something |= $built_lib;
    }
    $builder->run_hook('libraries.postbuild');
  }

  $config->append('includes', '"-I'.$config->get('builder.source.path').'"');

  if ($run_step->('sketch')) {
    info 'Building sketch...';
    $builder->run_hook('sketch.prebuild');
    # TODO: add configuration option for the ignored directories and also a way to
    # 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

# We just define this filter as the complement of the other one, to be sure not
# to loose any message.
log4perl.filter.DebugFilter = Log::Log4perl::Filter::LevelRange
log4perl.filter.DebugFilter.LevelMin = INFO
log4perl.filter.DebugFilter.LevelMax = FATAL
log4perl.filter.DebugFilter.AcceptOnMatch = false

log4perl.oneMessagePerAppender = 1


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.