# ------------------------------------------------------------------------------ # NAME # Fcm::Build # # DESCRIPTION # This is the top level class for the FCM build system. # # COPYRIGHT # (C) Crown copyright Met Office. All rights reserved. # For further details please refer to the file COPYRIGHT.txt # which you should have received as part of this distribution. # ------------------------------------------------------------------------------ use strict; use warnings; package Fcm::Build; use base qw(Fcm::ConfigSystem); use Carp qw{croak} ; use Cwd qw{cwd} ; use Fcm::BuildSrc ; use Fcm::BuildTask ; use Fcm::Config ; use Fcm::Dest ; use Fcm::CfgLine ; use Fcm::Timer qw{timestamp_command} ; use Fcm::Util qw{expand_tilde run_command touch_file w_report}; use File::Basename qw{dirname} ; use File::Spec ; use List::Util qw{first} ; use Text::ParseWords qw{shellwords} ; # List of scalar property methods for this class my @scalar_properties = ( 'name', # name of this build 'target', # targets of this build ); # List of hash property methods for this class my @hash_properties = ( 'srcpkg', # source packages of this build 'dummysrcpkg', # dummy for handling package inheritance with file extension ); # List of compare_setting_X methods my @compare_setting_methods = ( 'compare_setting_bld_blockdata', # program executable blockdata dependency 'compare_setting_bld_dep', # custom dependency setting 'compare_setting_bld_dep_excl', # exclude dependency setting 'compare_setting_bld_dep_n', # no dependency check 'compare_setting_bld_dep_pp', # custom PP dependency setting 'compare_setting_bld_dep_exe', # program executable extra dependency 'compare_setting_bld_exe_name', # program executable rename 'compare_setting_bld_pp', # PP flags 'compare_setting_infile_ext', # input file extension 'compare_setting_outfile_ext', # output file extension 'compare_setting_tool', # build tool settings ); my $DELIMITER_LIST = $Fcm::Config::DELIMITER_LIST; # ------------------------------------------------------------------------------ # SYNOPSIS # $obj = Fcm::Build->new; # # DESCRIPTION # This method constructs a new instance of the Fcm::Build class. # ------------------------------------------------------------------------------ sub new { my $this = shift; my %args = @_; my $class = ref $this || $this; my $self = Fcm::ConfigSystem->new (%args); $self->{$_} = undef for (@scalar_properties); $self->{$_} = {} for (@hash_properties); bless $self, $class; # List of sub-methods for parse_cfg push @{ $self->cfg_methods }, (qw/target source tool dep misc/); # Optional prefix in configuration declaration $self->cfg_prefix ($self->setting (qw/CFG_LABEL BDECLARE/)); # System type $self->type ('bld'); return $self; } # ------------------------------------------------------------------------------ # SYNOPSIS # $value = $obj->X; # $obj->X ($value); # # DESCRIPTION # Details of these properties are explained in @scalar_properties. # ------------------------------------------------------------------------------ for my $name (@scalar_properties) { no strict 'refs'; *$name = sub { my $self = shift; # Argument specified, set property to specified argument if (@_) { $self->{$name} = $_[0]; } # Default value for property if (not defined $self->{$name}) { if ($name eq 'target') { # Reference to an array $self->{$name} = []; } elsif ($name eq 'name') { # Empty string $self->{$name} = ''; } } return $self->{$name}; } } # ------------------------------------------------------------------------------ # SYNOPSIS # %hash = %{ $obj->X () }; # $obj->X (\%hash); # # $value = $obj->X ($index); # $obj->X ($index, $value); # # DESCRIPTION # Details of these properties are explained in @hash_properties. # # If no argument is set, this method returns a hash containing a list of # objects. If an argument is set and it is a reference to a hash, the objects # are replaced by the the specified hash. # # If a scalar argument is specified, this method returns a reference to an # object, if the indexed object exists or undef if the indexed object does # not exist. If a second argument is set, the $index element of the hash will # be set to the value of the argument. # ------------------------------------------------------------------------------ for my $name (@hash_properties) { no strict 'refs'; *$name = sub { my ($self, $arg1, $arg2) = @_; # Ensure property is defined as a reference to a hash $self->{$name} = {} if not defined ($self->{$name}); # Argument 1 can be a reference to a hash or a scalar index my ($index, %hash); if (defined $arg1) { if (ref ($arg1) eq 'HASH') { %hash = %$arg1; } else { $index = $arg1; } } if (defined $index) { # A scalar index is defined, set and/or return the value of an element $self->{$name}{$index} = $arg2 if defined $arg2; return ( exists $self->{$name}{$index} ? $self->{$name}{$index} : undef ); } else { # A scalar index is not defined, set and/or return the hash $self->{$name} = \%hash if defined $arg1; return $self->{$name}; } } } # ------------------------------------------------------------------------------ # SYNOPSIS # ($rc, $new_lines) = $self->X ($old_lines); # # DESCRIPTION # This method compares current settings with those in the cache, where X is # one of @compare_setting_methods. # # If setting has changed: # * For bld_blockdata, bld_dep_ext and bld_exe_name, it sets the re-generate # make-rule flag to true. # * For bld_dep_excl, in a standalone build, the method will remove the # dependency cache files for affected sub-packages. It returns an error if # the current build inherits from previous builds. # * For bld_pp, it updates the PP setting for affected sub-packages. # * For infile_ext, in a standalone build, the method will remove all the # sub-package cache files and trigger a re-build by removing most # sub-directories created by the previous build. It returns an error if the # current build inherits from previous builds. # * For outfile_ext, in a standalone build, the method will remove all the # sub-package dependency cache files. It returns an error if the current # build inherits from previous builds. # * For tool, it updates the "flags" files for any changed tools. # ------------------------------------------------------------------------------ for my $name (@compare_setting_methods) { no strict 'refs'; *$name = sub { my ($self, $old_lines) = @_; (my $prefix = uc ($name)) =~ s/^COMPARE_SETTING_//; my ($changed, $new_lines) = $self->compare_setting_in_config ($prefix, $old_lines); my $rc = scalar (keys %$changed); if ($rc and $old_lines) { $self->srcpkg ('')->is_updated (1); if ($name =~ /^compare_setting_bld_dep(?:_excl|_n|_pp)?$/) { # Mark affected packages as being updated for my $key (keys %$changed) { for my $pkg (values %{ $self->srcpkg }) { next unless $pkg->is_in_package ($key); $pkg->is_updated (1); } } } elsif ($name eq 'compare_setting_bld_pp') { # Mark affected packages as being updated for my $key (keys %$changed) { for my $pkg (values %{ $self->srcpkg }) { next unless $pkg->is_in_package ($key); next unless $self->srcpkg ($key)->is_type_any ( keys %{ $self->setting ('BLD_TYPE_DEP_PP') } ); # Is a type requiring pre-processing $pkg->is_updated (1); } } } elsif ($name eq 'compare_setting_infile_ext') { # Re-set input file type if necessary for my $key (keys %$changed) { for my $pkg (values %{ $self->srcpkg }) { next unless $pkg->src and $pkg->ext and $key eq $pkg->ext; $pkg->type (undef); } } # Mark affected packages as being updated for my $pkg (values %{ $self->srcpkg }) { $pkg->is_updated (1); } } elsif ($name eq 'compare_setting_outfile_ext') { # Mark affected packages as being updated for my $pkg (values %{ $self->srcpkg }) { $pkg->is_updated (1); } } elsif ($name eq 'compare_setting_tool') { # Update the "flags" files for changed tools for my $name (sort keys %$changed) { my ($tool, @names) = split /__/, $name; my $pkg = join ('__', @names); my @srcpkgs = $self->srcpkg ($pkg) ? ($self->srcpkg ($pkg)) : @{ $self->dummysrcpkg ($pkg)->children }; for my $srcpkg (@srcpkgs) { my $file = File::Spec->catfile ( $self->dest->flagsdir, $srcpkg->flagsbase ($tool) ); &touch_file ($file) or croak $file, ': cannot update, abort'; print $file, ': updated', "\n" if $self->verbose > 2; } } } } return ($rc, $new_lines); } } # ------------------------------------------------------------------------------ # SYNOPSIS # ($rc, $new_lines) = $self->compare_setting_dependency ($old_lines, $flag); # # DESCRIPTION # This method uses the previous settings to determine the dependencies of # current source files. # ------------------------------------------------------------------------------ sub compare_setting_dependency { my ($self, $old_lines, $flag) = @_; my $prefix = $flag ? 'DEP_PP' : 'DEP'; my $method = $flag ? 'ppdep' : 'dep'; my $rc = 0; my $new_lines = []; # Separate old lines my %old; if ($old_lines) { for my $line (@$old_lines) { next unless $line->label_starts_with ($prefix); $old{$line->label_from_field (1)} = $line; } } # Go through each source to see if the cache is up to date my $count = 0; my %mtime; for my $srcpkg (values %{ $self->srcpkg }) { next unless $srcpkg->cursrc and $srcpkg->type; my $key = $srcpkg->pkgname; my $out_of_date = $srcpkg->is_updated; # Check modification time of cache and source file if not out of date if (exists $old{$key}) { if (not $out_of_date) { $mtime{$old{$key}->src} = (stat ($old{$key}->src))[9] if not exists ($mtime{$old{$key}->src}); $out_of_date = 1 if $mtime{$old{$key}->src} < $srcpkg->curmtime; } } else { $out_of_date = 1; } if ($out_of_date) { # Re-scan dependency $srcpkg->is_updated(1); my ($source_is_read, $dep_hash_ref) = $srcpkg->get_dep($flag); if ($source_is_read) { $count++; } $srcpkg->$method($dep_hash_ref); $rc = 1; } else { # Use cached dependency my ($progname, %hash) = split ( /$Fcm::Config::DELIMITER_PATTERN/, $old{$key}->value ); $srcpkg->progname ($progname) if $progname and not $flag; $srcpkg->$method (\%hash); } # New lines values: progname[::dependency-name::type][...] my @value = ((defined $srcpkg->progname ? $srcpkg->progname : '')); for my $name (sort keys %{ $srcpkg->$method }) { push @value, $name, $srcpkg->$method ($name); } push @$new_lines, Fcm::CfgLine->new ( LABEL => $prefix . $Fcm::Config::DELIMITER . $key, VALUE => join ($Fcm::Config::DELIMITER, @value), ); } print 'No. of file', ($count > 1 ? 's' : ''), ' scanned for', ($flag ? ' PP': ''), ' dependency: ', $count, "\n" if $self->verbose and $count; return ($rc, $new_lines); } # ------------------------------------------------------------------------------ # SYNOPSIS # ($rc, $new_lines) = $self->compare_setting_srcpkg ($old_lines); # # DESCRIPTION # This method uses the previous settings to determine the type of current # source files. # ------------------------------------------------------------------------------ sub compare_setting_srcpkg { my ($self, $old_lines) = @_; my $prefix = 'SRCPKG'; # Get relevant items from old lines, stripping out $prefix my %old; if ($old_lines) { for my $line (@$old_lines) { next unless $line->label_starts_with ($prefix); $old{$line->label_from_field (1)} = $line; } } # Check for change, use previous setting if exist my $out_of_date = 0; my %mtime; for my $key (keys %{ $self->srcpkg }) { if (exists $old{$key}) { next unless $self->srcpkg ($key)->cursrc; my $type = defined $self->setting ('BLD_TYPE', $key) ? $self->setting ('BLD_TYPE', $key) : $old{$key}->value; $self->srcpkg ($key)->type ($type); if ($type ne $old{$key}->value) { $self->srcpkg ($key)->is_updated (1); $out_of_date = 1; } if (not $self->srcpkg ($key)->is_updated) { $mtime{$old{$key}->src} = (stat ($old{$key}->src))[9] if not exists ($mtime{$old{$key}->src}); $self->srcpkg ($key)->is_updated (1) if $mtime{$old{$key}->src} < $self->srcpkg ($key)->curmtime; } } else { $self->srcpkg ($key)->is_updated (1); $out_of_date = 1; } } # Check for deleted keys for my $key (keys %old) { next if $self->srcpkg ($key); $out_of_date = 1; } # Return reference to an array of new lines my $new_lines = []; for my $key (keys %{ $self->srcpkg }) { push @$new_lines, Fcm::CfgLine->new ( LABEL => $prefix . $Fcm::Config::DELIMITER . $key, VALUE => $self->srcpkg ($key)->type, ); } return ($out_of_date, $new_lines); } # ------------------------------------------------------------------------------ # SYNOPSIS # ($rc, $new_lines) = $self->compare_setting_target ($old_lines); # # DESCRIPTION # This method compare the previous target settings with current ones. # ------------------------------------------------------------------------------ sub compare_setting_target { my ($self, $old_lines) = @_; my $prefix = 'TARGET'; my $old; if ($old_lines) { for my $line (@$old_lines) { next unless $line->label_starts_with ($prefix); $old = $line->value; last; } } my $new = join (' ', sort @{ $self->target }); return ( (defined ($old) ? $old ne $new : 1), [Fcm::CfgLine->new (LABEL => $prefix, VALUE => $new)], ); } # ------------------------------------------------------------------------------ # SYNOPSIS # $rc = $self->invoke_fortran_interface_generator (); # # DESCRIPTION # This method invokes the Fortran interface generator for all Fortran free # format source files. It returns true on success. # ------------------------------------------------------------------------------ sub invoke_fortran_interface_generator { my $self = shift; my $pdoneext = $self->setting (qw/OUTFILE_EXT PDONE/); # Set up build task to generate interface files for all selected Fortran 9x # sources my %task = (); SRC_FILE: for my $srcfile (values %{ $self->srcpkg }) { if (!defined($srcfile->interfacebase())) { next SRC_FILE; } my $target = $srcfile->interfacebase . $pdoneext; $task{$target} = Fcm::BuildTask->new ( TARGET => $target, TARGETPATH => $self->dest->donepath, SRCFILE => $srcfile, DEPENDENCY => [$srcfile->flagsbase ('GENINTERFACE')], ACTIONTYPE => 'GENINTERFACE', ); # Set up build tasks for each source file/package flags file for interface # generator tool for my $i (1 .. @{ $srcfile->pkgnames }) { my $target = $srcfile->flagsbase ('GENINTERFACE', -$i); my $depend = $i < @{ $srcfile->pkgnames } ? $srcfile->flagsbase ('GENINTERFACE', -$i - 1) : undef; $task{$target} = Fcm::BuildTask->new ( TARGET => $target, TARGETPATH => $self->dest->flagspath, DEPENDENCY => [defined ($depend) ? $depend : ()], ACTIONTYPE => 'UPDATE', ) if not exists $task{$target}; } } # Set up build task to update the flags file for interface generator tool $task{$self->srcpkg ('')->flagsbase ('GENINTERFACE')} = Fcm::BuildTask->new ( TARGET => $self->srcpkg ('')->flagsbase ('GENINTERFACE'), TARGETPATH => $self->dest->flagspath, ACTIONTYPE => 'UPDATE', ); my $count = 0; # Performs task for my $task (values %task) { next unless $task->actiontype eq 'GENINTERFACE'; my $rc = $task->action (TASKLIST => \%task); $count++ if $rc; } print 'No. of generated Fortran interface', ($count > 1 ? 's' : ''), ': ', $count, "\n" if $self->verbose and $count; return 1; } # ------------------------------------------------------------------------------ # SYNOPSIS # $rc = $self->invoke_make (%args); # # DESCRIPTION # This method invokes the make stage of the build system. It returns true on # success. # # ARGUMENTS # ARCHIVE - If set to "true", invoke the "archive" mode. Most build files and # directories created by this build will be archived using the # "tar" command. If not set, the default is not to invoke the # "archive" mode. # JOBS - Specify number of jobs that can be handled by "make". If set, the # value must be a natural integer. If not set, the default value is # 1 (i.e. run "make" in serial mode). # TARGETS - Specify targets to be built. If set, these targets will be built # instead of the ones specified in the build configuration file. # ------------------------------------------------------------------------------ sub invoke_make { my ($self, %args) = @_; $args{TARGETS} ||= ['all']; $args{JOBS} ||= 1; my @command = ( $self->setting(qw/TOOL MAKE/), shellwords($self->setting(qw/TOOL MAKEFLAGS/)), # -f Makefile ($self->setting(qw/TOOL MAKE_FILE/), $self->dest()->bldmakefile()), # -j N ($args{JOBS} ? ($self->setting(qw/TOOL MAKE_JOB/), $args{JOBS}) : ()), # -s ($self->verbose() >= 3 ? $self->setting(qw/TOOL MAKE_SILENT/) : ()), @{$args{TARGETS}} ); my $old_cwd = $self->_chdir($self->dest()->rootdir()); run_command( \@command, ERROR => 'warn', RC => \my($code), TIME => $self->verbose() >= 3, ); $self->_chdir($old_cwd); my $rc = !$code; if ($rc && $args{ARCHIVE}) { $rc = $self->dest()->archive(); } $rc &&= $self->dest()->create_bldrunenvsh(); while (my ($key, $source) = each(%{$self->srcpkg()})) { $rc &&= defined($source->write_lib_dep_excl()); } return $rc; } # ------------------------------------------------------------------------------ # SYNOPSIS # $rc = $self->invoke_pre_process (); # # DESCRIPTION # This method invokes the pre-process stage of the build system. It # returns true on success. # ------------------------------------------------------------------------------ sub invoke_pre_process { my $self = shift; # Check whether pre-processing is necessary my $invoke = 0; for (values %{ $self->srcpkg }) { next unless $_->get_setting ('BLD_PP'); $invoke = 1; last; } return 1 unless $invoke; # Scan header dependency my $rc = $self->compare_setting ( METHOD_LIST => ['compare_setting_dependency'], METHOD_ARGS => ['BLD_TYPE_DEP_PP'], CACHEBASE => $self->setting ('CACHE_DEP_PP'), ); return $rc if not $rc; my %task = (); my $pdoneext = $self->setting (qw/OUTFILE_EXT PDONE/); # Set up tasks for each source file for my $srcfile (values %{ $self->srcpkg }) { if ($srcfile->is_type_all (qw/CPP INCLUDE/)) { # Set up a copy build task for each include file $task{$srcfile->base} = Fcm::BuildTask->new ( TARGET => $srcfile->base, TARGETPATH => $self->dest->incpath, SRCFILE => $srcfile, DEPENDENCY => [keys %{ $srcfile->ppdep }], ACTIONTYPE => 'COPY', ); } elsif ($srcfile->lang ('TOOL_SRC_PP')) { next unless $srcfile->get_setting ('BLD_PP'); # Set up a PP build task for each source file my $target = $srcfile->base . $pdoneext; # Issue warning for duplicated tasks if (exists $task{$target}) { w_report 'WARNING: ', $target, ': unable to create task for: ', $srcfile->src, ': task already exists for: ', $task{$target}->srcfile->src; next; } $task{$target} = Fcm::BuildTask->new ( TARGET => $target, TARGETPATH => $self->dest->donepath, SRCFILE => $srcfile, DEPENDENCY => [$srcfile->flagsbase ('PPKEYS'), keys %{ $srcfile->ppdep }], ACTIONTYPE => 'PP', ); # Set up update ppkeys/flags build tasks for each source file/package my $ppkeys = $self->setting ( 'TOOL_SRC_PP', $srcfile->lang ('TOOL_SRC_PP'), 'PPKEYS' ); for my $i (1 .. @{ $srcfile->pkgnames }) { my $target = $srcfile->flagsbase ($ppkeys, -$i); my $depend = $i < @{ $srcfile->pkgnames } ? $srcfile->flagsbase ($ppkeys, -$i - 1) : undef; $task{$target} = Fcm::BuildTask->new ( TARGET => $target, TARGETPATH => $self->dest->flagspath, DEPENDENCY => [defined ($depend) ? $depend : ()], ACTIONTYPE => 'UPDATE', ) if not exists $task{$target}; } } } # Set up update global ppkeys build tasks for my $lang (keys %{ $self->setting ('TOOL_SRC_PP') }) { my $target = $self->srcpkg ('')->flagsbase ( $self->setting ('TOOL_SRC_PP', $lang, 'PPKEYS') ); $task{$target} = Fcm::BuildTask->new ( TARGET => $target, TARGETPATH => $self->dest->flagspath, ACTIONTYPE => 'UPDATE', ); } # Build all PP tasks my $count = 0; for my $task (values %task) { next unless $task->actiontype eq 'PP'; my $rc = $task->action (TASKLIST => \%task); $task->srcfile->is_updated ($rc); $count++ if $rc; } print 'No. of pre-processed file', ($count > 1 ? 's' : ''), ': ', $count, "\n" if $self->verbose and $count; return 1; } # ------------------------------------------------------------------------------ # SYNOPSIS # $rc = $self->invoke_scan_dependency (); # # DESCRIPTION # This method invokes the scan dependency stage of the build system. It # returns true on success. # ------------------------------------------------------------------------------ sub invoke_scan_dependency { my $self = shift; # Scan/retrieve dependency # ---------------------------------------------------------------------------- my $rc = $self->compare_setting ( METHOD_LIST => ['compare_setting_dependency'], CACHEBASE => $self->setting ('CACHE_DEP'), ); # Check whether make file is out of date # ---------------------------------------------------------------------------- my $out_of_date = not -r $self->dest->bldmakefile; if ($rc and not $out_of_date) { for (qw/CACHE CACHE_DEP/) { my $cache_mtime = (stat (File::Spec->catfile ( $self->dest->cachedir, $self->setting ($_), )))[9]; my $mfile_mtime = (stat ($self->dest->bldmakefile))[9]; next if not defined $cache_mtime; next if $cache_mtime < $mfile_mtime; $out_of_date = 1; last; } } if ($rc and not $out_of_date) { for (values %{ $self->srcpkg }) { next unless $_->is_updated; $out_of_date = 1; last; } } if ($rc and $out_of_date) { # Write Makefile # -------------------------------------------------------------------------- # Register non-word package name my $unusual = 0; for my $key (sort keys %{ $self->srcpkg }) { next if $self->srcpkg ($key)->src; next if $key =~ /^\w*$/; $self->setting ( ['FCM_PCK_OBJECTS', $key], 'FCM_PCK_OBJECTS' . $unusual++, ); } # Write different parts in the Makefile my $makefile = '# Automatic Makefile' . "\n\n"; $makefile .= 'FCM_BLD_NAME = ' . $self->name . "\n" if $self->name; $makefile .= 'FCM_BLD_CFG = ' . $self->cfg->actual_src . "\n"; $makefile .= 'export FCM_VERBOSE ?= ' . $self->verbose . "\n\n"; $makefile .= $self->dest->write_rules; $makefile .= $self->_write_makefile_perl5lib; $makefile .= $self->_write_makefile_tool; $makefile .= $self->_write_makefile_vpath; $makefile .= $self->_write_makefile_target; # Write rules for each source package # Ensure that container packages come before files - this allows $(OBJECTS) # and its dependent variables to expand correctly my @srcpkg = sort { if ($self->srcpkg ($a)->libbase and $self->srcpkg ($b)->libbase) { $b cmp $a; } elsif ($self->srcpkg ($a)->libbase) { -1; } elsif ($self->srcpkg ($b)->libbase) { 1; } else { $a cmp $b; } } keys %{ $self->srcpkg }; for (@srcpkg) { $makefile .= $self->srcpkg ($_)->write_rules if $self->srcpkg ($_)->rules; } $makefile .= '# EOF' . "\n"; # Update Makefile open OUT, '>', $self->dest->bldmakefile or croak $self->dest->bldmakefile, ': cannot open (', $!, '), abort'; print OUT $makefile; close OUT or croak $self->dest->bldmakefile, ': cannot close (', $!, '), abort'; print $self->dest->bldmakefile, ': updated', "\n" if $self->verbose; # Check for duplicated targets # -------------------------------------------------------------------------- # Get list of types that cannot have duplicated targets my @no_duplicated_target_types = split ( /$DELIMITER_LIST/, $self->setting ('BLD_TYPE_NO_DUPLICATED_TARGET'), ); my %targets; for my $name (sort keys %{ $self->srcpkg }) { next unless $self->srcpkg ($name)->rules; for my $key (sort keys %{ $self->srcpkg ($name)->rules }) { if (exists $targets{$key}) { # Duplicated target: warning for most file types my $status = 'WARNING'; # Duplicated target: error for the following file types if (@no_duplicated_target_types and $self-srcpkg ($name)->is_type_any (@no_duplicated_target_types) and $targets{$key}->is_type_any (@no_duplicated_target_types)) { $status = 'ERROR'; $rc = 0; } # Report the warning/error w_report $status, ': ', $key, ': duplicated targets for building:'; w_report ' ', $targets{$key}->src; w_report ' ', $self->srcpkg ($name)->src; } else { $targets{$key} = $self->srcpkg ($name); } } } } return $rc; } # ------------------------------------------------------------------------------ # SYNOPSIS # $rc = $self->invoke_setup_build (); # # DESCRIPTION # This method invokes the setup_build stage of the build system. It returns # true on success. # ------------------------------------------------------------------------------ sub invoke_setup_build { my $self = shift; my $rc = 1; # Extract archived sub-directories if necessary $rc = $self->dest->dearchive if $rc; # Compare cache $rc = $self->compare_setting (METHOD_LIST => [ 'compare_setting_target', # targets 'compare_setting_srcpkg', # source package type @compare_setting_methods, ]) if $rc; # Set up runtime dependency scan patterns my %dep_pattern = %{ $self->setting ('BLD_DEP_PATTERN') }; for my $key (keys %dep_pattern) { my $pattern = $dep_pattern{$key}; while ($pattern =~ /##([\w:]+)##/g) { my $match = $1; my $val = $self->setting (split (/$Fcm::Config::DELIMITER/, $match)); last unless defined $val; $val =~ s/\./\\./; $pattern =~ s/##$match##/$val/; } $self->setting (['BLD_DEP_PATTERN', $key], $pattern) unless $pattern eq $dep_pattern{$key}; } return $rc; } # ------------------------------------------------------------------------------ # SYNOPSIS # $rc = $self->invoke_system (%args); # # DESCRIPTION # This method invokes the build system. It returns true on success. See also # the header for invoke_make for further information on arguments. # # ARGUMENTS # STAGE - If set, it should be an integer number or a recognised keyword or # abbreviation. If set, the build is performed up to the named stage. # If not set, the default is to perform all stages of the build. # Allowed values are: # 1, setup or s # 2, pre_process or pp # 3, generate_dependency or gd # 4, generate_interface or gi # 5, all, a, make or m # ------------------------------------------------------------------------------ sub invoke_system { my $self = shift; my %args = @_; # Parse arguments # ---------------------------------------------------------------------------- # Default: run all 5 stages my $stage = (exists $args{STAGE} and $args{STAGE}) ? $args{STAGE} : 5; # Resolve named stages if ($stage !~ /^\d$/) { my %stagenames = ( 'S(?:ETUP)?' => 1, 'P(?:RE)?_?P(?:ROCESS)?' => 2, 'G(?:ENERATE)?_?D(?:ENPENDENCY)?' => 3, 'G(?:ENERATE)?_?I(?:NTERFACE)?' => 4, '(?:A(?:LL)|M(?:AKE)?)' => 5, ); # Does it match a recognised stage? for my $name (keys %stagenames) { next unless $stage =~ /$name/i; $stage = $stagenames{$name}; last; } # Specified stage name not recognised, default to 5 if ($stage !~ /^\d$/) { w_report 'WARNING: ', $stage, ': invalid build stage, default to 5.'; $stage = 5; } } # Run the method associated with each stage # ---------------------------------------------------------------------------- my $rc = 1; my @stages = ( ['Setup build' , 'invoke_setup_build'], ['Pre-process' , 'invoke_pre_process'], ['Scan dependency' , 'invoke_scan_dependency'], ['Generate Fortran interface', 'invoke_fortran_interface_generator'], ['Make' , 'invoke_make'], ); for my $i (1 .. 5) { last if (not $rc) or $i > $stage; my ($name, $method) = @{ $stages[$i - 1] }; $rc = $self->invoke_stage ($name, $method, %args) if $rc and $stage >= $i; } return $rc; } # ------------------------------------------------------------------------------ # SYNOPSIS # $rc = $self->parse_cfg_dep (\@cfg_lines); # # DESCRIPTION # This method parses the dependency settings in the @cfg_lines. # ------------------------------------------------------------------------------ sub parse_cfg_dep { my ($self, $cfg_lines) = @_; my $rc = 1; # EXCL_DEP, EXE_DEP and BLOCKDATA declarations # ---------------------------------------------------------------------------- for my $name (qw/BLD_BLOCKDATA BLD_DEP BLD_DEP_EXCL BLD_DEP_EXE/) { for my $line (grep {$_->slabel_starts_with_cfg ($name)} @$cfg_lines) { # Separate label into a list, delimited by double-colon, remove 1st field my @flds = $line->slabel_fields; shift @flds; if ($name =~ /^(?:BLD_DEP|BLD_DEP_EXCL|BLD_DEP_PP)$/) { # BLD_DEP_*: label fields may contain sub-package my $pk = @flds ? join ('__', @flds) : ''; # Check whether sub-package is valid if ($pk and not ($self->srcpkg ($pk) or $self->dummysrcpkg ($pk))) { $line->error ($line->label . ': invalid sub-package in declaration.'); $rc = 0; next; } # Setting is stored in an array reference $self->setting ([$name, $pk], []) if not defined $self->setting ($name, $pk); # Add current declaration to the array if necessary my $list = $self->setting ($name, $pk); my $value = $name eq 'BLD_DEP_EXCL' ? uc ($line->value) : $line->value; push @$list, $value if not grep {$_ eq $value} @$list; } else { # EXE_DEP and BLOCKDATA: label field may be an executable target my $target = @flds ? $flds[0] : ''; # The value contains a list of objects and/or sub-package names my @deps = split /\s+/, $line->value; if (not @deps) { if ($name eq 'BLD_BLOCKDATA') { # The objects containing a BLOCKDATA program unit must be declared $line->error ($line->label . ': value not set.'); $rc = 0; next; } else { # If $value is a null string, target(s) depends on all objects push @deps, ''; } } for my $dep (@deps) { $dep =~ s/$Fcm::Config::DELIMITER_PATTERN/__/g; } $self->setting ([$name, $target], join (' ', sort @deps)); } $line->parsed (1); } } return $rc; } # ------------------------------------------------------------------------------ # SYNOPSIS # $rc = $self->parse_cfg_dest (\@cfg_lines); # # DESCRIPTION # This method parses the build destination settings in the @cfg_lines. # ------------------------------------------------------------------------------ sub parse_cfg_dest { my ($self, $cfg_lines) = @_; my $rc = $self->SUPER::parse_cfg_dest ($cfg_lines); # Set up search paths for my $name (@Fcm::Dest::paths) { (my $label = uc ($name)) =~ s/PATH//; $self->setting (['PATH', $label], $self->dest->$name); } return $rc; } # ------------------------------------------------------------------------------ # SYNOPSIS # $rc = $self->parse_cfg_misc (\@cfg_lines); # # DESCRIPTION # This method parses misc build settings in the @cfg_lines. # ------------------------------------------------------------------------------ sub parse_cfg_misc { my ($self, $cfg_lines_ref) = @_; my $rc = 1; my %item_of = ( BLD_DEP_N => [\&_parse_cfg_misc_dep_n , 1 ], # boolean BLD_EXE_NAME => [\&_parse_cfg_misc_exe_name ], BLD_LIB => [\&_parse_cfg_misc_dep_n ], BLD_PP => [\&_parse_cfg_misc_dep_n , 1 ], # boolean BLD_TYPE => [\&_parse_cfg_misc_dep_n ], INFILE_EXT => [\&_parse_cfg_misc_file_ext, 0, 1], # uc($value) OUTFILE_EXT => [\&_parse_cfg_misc_file_ext, 1, 0], # uc($ns) ); while (my ($key, $item) = each(%item_of)) { my ($handler, @extra_arguments) = @{$item}; for my $line (@{$cfg_lines_ref}) { if ($line->slabel_starts_with_cfg($key)) { if ($handler->($self, $key, $line, @extra_arguments)) { $line->parsed(1); } else { $rc = 0; } } } } return $rc; } # ------------------------------------------------------------------------------ # parse_cfg_misc: handler of BLD_EXE_NAME or similar. sub _parse_cfg_misc_exe_name { my ($self, $key, $line) = @_; my ($prefix, $name, @fields) = $line->slabel_fields(); if (!$name || @fields) { $line->error(sprintf('%s: expects a single label name field.', $key)); return 0; } $self->setting([$key, $name], $line->value()); return 1; } # ------------------------------------------------------------------------------ # parse_cfg_misc: handler of BLD_DEP_N or similar. sub _parse_cfg_misc_dep_n { my ($self, $key, $line, $value_is_boolean) = @_; my ($prefix, @fields) = $line->slabel_fields(); my $ns = @fields ? join(q{__}, @fields) : q{}; if ($ns && !$self->srcpkg($ns) && !$self->dummysrcpkg($ns)) { $line->error($line->label() . ': invalid sub-package in declaration.'); return 0; } my @srcpkgs = $self->dummysrcpkg($ns) ? @{$self->dummysrcpkg($ns)->children()} : $self->srcpkg($ns) ; my $value = $value_is_boolean ? $line->bvalue() : $line->value(); for my $srcpkg (@srcpkgs) { $self->setting([$key, $srcpkg->pkgname()], $value); } return 1; } # ------------------------------------------------------------------------------ # parse_cfg_misc: handler of INFILE_EXT/OUTFILE_EXT or similar. sub _parse_cfg_misc_file_ext { my ($self, $key, $line, $ns_in_uc, $value_in_uc) = @_; my ($prefix, $ns) = $line->slabel_fields(); my $value = $value_in_uc ? uc($line->value()) : $line->value(); $self->setting([$key, ($ns_in_uc ? uc($ns) : $ns)], $value); return 1; } # ------------------------------------------------------------------------------ # SYNOPSIS # $rc = $self->parse_cfg_source (\@cfg_lines); # # DESCRIPTION # This method parses the source package settings in the @cfg_lines. # ------------------------------------------------------------------------------ sub parse_cfg_source { my ($self, $cfg_lines) = @_; my $rc = 1; my %src = (); # Automatic source directory search? # ---------------------------------------------------------------------------- my $search = 1; for my $line (grep {$_->slabel_starts_with_cfg ('SEARCH_SRC')} @$cfg_lines) { $search = $line->bvalue; $line->parsed (1); } # Search src/ sub-directory if necessary %src = %{ $self->dest->get_source_files } if $search; # SRC declarations # ---------------------------------------------------------------------------- for my $line (grep {$_->slabel_starts_with_cfg ('FILE')} @$cfg_lines) { # Expand ~ notation and path relative to srcdir of destination my $value = $line->value; $value = File::Spec->rel2abs (&expand_tilde ($value), $self->dest->srcdir); if (not -r $value) { $line->error ($value . ': source does not exist or is not readable.'); next; } # Package name my @names = $line->slabel_fields; shift @names; # If package name not set, determine using the path if possible if (not @names) { my $package = $self->dest->get_pkgname_of_path ($value); @names = @$package if defined $package; } if (not @names) { $line->error ($self->cfglabel ('FILE') . ': package not specified/cannot be determined.'); next; } $src{join ('__', @names)} = $value; $line->parsed (1); } # For directories, get non-recursive file listing, and add to %src # ---------------------------------------------------------------------------- for my $key (keys %src) { next unless -d $src{$key}; opendir DIR, $src{$key} or die $src{$key}, ': cannot read directory'; while (my $base = readdir 'DIR') { next if $base =~ /^\./; my $file = File::Spec->catfile ($src{$key}, $base); next unless -f $file and -r $file; my $name = join ('__', ($key, $base)); $src{$name} = $file unless exists $src{$name}; } closedir DIR; delete $src{$key}; } # Set up source packages # ---------------------------------------------------------------------------- my %pkg = (); for my $name (keys %src) { $pkg{$name} = Fcm::BuildSrc->new (PKGNAME => $name, SRC => $src{$name}); } # INHERIT::SRC declarations # ---------------------------------------------------------------------------- my %can_inherit = (); for my $line ( grep {$_->slabel_starts_with_cfg(qw/INHERIT FILE/)} @{$cfg_lines} ) { my ($key1, $key2, @ns) = $line->slabel_fields(); $can_inherit{join('__', @ns)} = $line->bvalue(); $line->parsed(1); } # Inherit packages, if it is OK to do so for my $inherited_build (reverse(@{$self->inherit()})) { SRCPKG: while (my ($key, $srcpkg) = each(%{$inherited_build->srcpkg()})) { if (exists($pkg{$key}) || !$srcpkg->src()) { next SRCPKG; } my $known_key = first {exists($can_inherit{$_})} @{$srcpkg->pkgnames()}; if (defined($known_key) && !$can_inherit{$known_key}) { next SRCPKG; } $pkg{$key} = $srcpkg; } } # Get list of intermediate "packages" # ---------------------------------------------------------------------------- for my $name (keys %pkg) { # Name of current package my @names = split /__/, $name; my $cur = $name; while ($cur) { # Name of parent package pop @names; my $parent = @names ? join ('__', @names) : ''; # If parent package does not exist, create it $pkg{$parent} = Fcm::BuildSrc->new (PKGNAME => $parent) unless exists $pkg{$parent}; # Current package is a child of the parent package push @{ $pkg{$parent}->children }, $pkg{$cur} unless grep {$_->pkgname eq $cur} @{ $pkg{$parent}->children }; # Go up a package $cur = $parent; } } $self->srcpkg (\%pkg); # Dummy: e.g. "foo/bar/baz.egg" belongs to the "foo/bar/baz" dummy. # ---------------------------------------------------------------------------- for my $name (keys %pkg) { (my $dname = $name) =~ s/\.\w+$//; next if $dname eq $name; next if $self->srcpkg ($dname); $self->dummysrcpkg ($dname, Fcm::BuildSrc->new (PKGNAME => $dname)) unless $self->dummysrcpkg ($dname); push @{ $self->dummysrcpkg ($dname)->children }, $pkg{$name}; } # Make sure a package is defined # ---------------------------------------------------------------------------- if (not %{$self->srcpkg}) { w_report 'ERROR: ', $self->cfg->actual_src, ': no source file to build.'; $rc = 0; } return $rc; } # ------------------------------------------------------------------------------ # SYNOPSIS # $rc = $self->parse_cfg_target (\@cfg_lines); # # DESCRIPTION # This method parses the target settings in the @cfg_lines. # ------------------------------------------------------------------------------ sub parse_cfg_target { my ($self, $cfg_lines) = @_; # NAME declaraions # ---------------------------------------------------------------------------- for my $line (grep {$_->slabel_starts_with_cfg ('NAME')} @$cfg_lines) { $self->name ($line->value); $line->parsed (1); } # TARGET declarations # ---------------------------------------------------------------------------- for my $line (grep {$_->slabel_starts_with_cfg ('TARGET')} @$cfg_lines) { # Value is a space delimited list push @{ $self->target }, split (/\s+/, $line->value); $line->parsed (1); } # INHERIT::TARGET declarations # ---------------------------------------------------------------------------- # By default, do not inherit target my $inherit_flag = 0; for (grep {$_->slabel_starts_with_cfg (qw/INHERIT TARGET/)} @$cfg_lines) { $inherit_flag = $_->bvalue; $_->parsed (1); } # Inherit targets from inherited build, if $inherit_flag is set to true # ---------------------------------------------------------------------------- if ($inherit_flag) { for my $use (reverse @{ $self->inherit }) { unshift @{ $self->target }, @{ $use->target }; } } return 1; } # ------------------------------------------------------------------------------ # SYNOPSIS # $rc = $self->parse_cfg_tool (\@cfg_lines); # # DESCRIPTION # This method parses the tool settings in the @cfg_lines. # ------------------------------------------------------------------------------ sub parse_cfg_tool { my ($self, $cfg_lines) = @_; my $rc = 1; my %tools = %{ $self->setting ('TOOL') }; my @package_tools = split(/$DELIMITER_LIST/, $self->setting('TOOL_PACKAGE')); # TOOL declaration # ---------------------------------------------------------------------------- for my $line (grep {$_->slabel_starts_with_cfg ('TOOL')} @$cfg_lines) { # Separate label into a list, delimited by double-colon, remove TOOL my @flds = $line->slabel_fields; shift @flds; # Check that there is a field after TOOL if (not @flds) { $line->error ('TOOL: not followed by a valid label.'); $rc = 0; next; } # The first field is the tool iteself, identified in uppercase $flds[0] = uc ($flds[0]); # Check that the tool is recognised if (not exists $tools{$flds[0]}) { $line->error ($flds[0] . ': not a valid TOOL.'); $rc = 0; next; } # Check sub-package declaration if (@flds > 1 and not grep {$_ eq $flds[0]} @package_tools) { $line->error ($flds[0] . ': sub-package not accepted with this TOOL.'); $rc = 0; next; } # Name of declared package my $pk = join ('__', @flds[1 .. $#flds]); # Check whether package exists if (not ($self->srcpkg ($pk) or $self->dummysrcpkg ($pk))) { $line->error ($line->label . ': invalid sub-package in declaration.'); $rc = 0; next; } $self->setting (['TOOL', join ('__', @flds)], $line->value); $line->parsed (1); } return $rc; } # ------------------------------------------------------------------------------ # SYNOPSIS # $string = $self->_write_makefile_perl5lib (); # # DESCRIPTION # This method returns a makefile $string for defining $PERL5LIB. # ------------------------------------------------------------------------------ sub _write_makefile_perl5lib { my $self = shift; my $classpath = File::Spec->catfile (split (/::/, ref ($self))) . '.pm'; my $libdir = dirname (dirname ($INC{$classpath})); my @libpath = split (/:/, (exists $ENV{PERL5LIB} ? $ENV{PERL5LIB} : '')); my $string = ((grep {$_ eq $libdir} @libpath) ? '' : 'export PERL5LIB := ' . $libdir . (exists $ENV{PERL5LIB} ? ':$(PERL5LIB)' : '') . "\n\n"); return $string; } # ------------------------------------------------------------------------------ # SYNOPSIS # $string = $self->_write_makefile_target (); # # DESCRIPTION # This method returns a makefile $string for defining the default targets. # ------------------------------------------------------------------------------ sub _write_makefile_target { my $self = shift; # Targets of the build # ---------------------------------------------------------------------------- my @targets = @{ $self->target }; if (not @targets) { # Build targets not specified by user, default to building all main programs my @programs = (); # Get all main programs from all packages for my $pkg (values %{ $self->srcpkg }) { push @programs, $pkg->exebase if $pkg->exebase; } @programs = sort (@programs); if (@programs) { # Build main programs, if there are any @targets = @programs; } else { # No main program in source tree, build the default library @targets = ($self->srcpkg ('')->libbase); } } my $return = 'FCM_BLD_TARGETS = ' . join (' ', @targets) . "\n\n"; # Default targets $return .= '.PHONY : all' . "\n\n"; $return .= 'all : $(FCM_BLD_TARGETS)' . "\n\n"; # Targets for copy dummy $return .= sprintf("%s:\n\ttouch \$@\n\n", $self->setting(qw/BLD_CPDUMMY/)); return $return; } # ------------------------------------------------------------------------------ # SYNOPSIS # $string = $self->_write_makefile_tool (); # # DESCRIPTION # This method returns a makefile $string for defining the build tools. # ------------------------------------------------------------------------------ sub _write_makefile_tool { my $self = shift; # List of build tools my $tool = $self->setting ('TOOL'); # List of tools local to FCM, (will not be exported) my %localtool = map {($_, 1)} split ( # map into a hash table /$DELIMITER_LIST/, $self->setting ('TOOL_LOCAL'), ); # Export required tools my $count = 0; my $return = ''; for my $name (sort keys %$tool) { # Ignore local tools next if exists $localtool{(split (/__/, $name))[0]}; if ($name =~ /^\w+$/) { # Tools with normal name, just export it as an environment variable $return .= 'export ' . $name . ' = ' . $tool->{$name} . "\n"; } else { # Tools with unusual characters, export using a label/value pair $return .= 'export FCM_UNUSUAL_TOOL_LABEL' . $count . ' = ' . $name . "\n"; $return .= 'export FCM_UNUSUAL_TOOL_VALUE' . $count . ' = ' . $tool->{$name} . "\n"; $count++; } } $return .= "\n"; return $return; } # ------------------------------------------------------------------------------ # SYNOPSIS # $string = $self->_write_makefile_vpath (); # # DESCRIPTION # This method returns a makefile $string for defining vpath directives. # ------------------------------------------------------------------------------ sub _write_makefile_vpath { my $self = shift(); my $FMT = 'vpath %%%s $(FCM_%sPATH)'; my %SETTING_OF = %{$self->setting('BLD_VPATH')}; my %EXT_OF = %{$self->setting('OUTFILE_EXT')}; # Note: each setting can be either an empty string or a comma-separated list # of output file extension keys. join( "\n", ( map { my $key = $_; my @types = split(qr{$DELIMITER_LIST}msx, $SETTING_OF{$key}); @types ? (map {sprintf($FMT, $EXT_OF{$_}, $key)} sort @types) : sprintf($FMT, q{}, $key) ; } sort keys(%SETTING_OF) ), ) . "\n\n"; } # Wraps chdir. Returns the old working directory. sub _chdir { my ($self, $path) = @_; if ($self->verbose() >= 3) { printf("cd %s\n", $path); } my $old_cwd = cwd(); chdir($path) || croak(sprintf("%s: cannot change directory ($!)\n", $path)); $old_cwd; } # ------------------------------------------------------------------------------ 1; __END__