package RpmSpecWorkspaceCreator; # ************************************************************ # Description : An RPM .spec file Workspace Creator # Author : Adam Mitz (OCI) # Create Date : 11/23/2010 # ************************************************************ # ************************************************************ # Pragmas # ************************************************************ use strict; use File::Path; use POSIX qw(strftime); use RpmSpecProjectCreator; use WorkspaceCreator; use vars qw(@ISA); @ISA = qw(WorkspaceCreator); # ************************************************************ # Data Section # ************************************************************ my $ext = '.spec'; # extension of files written by this WorkspaceCreator # ************************************************************ # Subroutine Section # ************************************************************ sub workspace_file_name { my $self = shift; return $self->get_modified_workspace_name($self->get_workspace_name(), $ext); } # Called by document_template.pl sub documentation_info { shift; #ignore package name my $keywords = shift; %$keywords = ('apply' => \&interpret_keyword, 'cond' => \&interpret_keyword); return '^sub get_template', '^EOT$'; } sub default_verbose_ordering { return 1; # Warn if there are missing dependencies. } # Called by document_template.pl sub interpret_keyword { my $vname = shift; $vname = (split /,/, $vname)[0]; return ($vname, $vname, $vname, undef); } # Don't actually write the .spec file for the workspace. Instead just invoke # the $func callback so that post_workspace() and other parts of the normal # workspace processing are called. We don't want a .spec file for each MPC # workspace because that is too course-grained. Instead, post_workspace() will # create one .spec for each aggregated workspace inside the primary workspace. # Using the workspace aggregation mechanism this way allows multiple .spec # files per workspace with MPC deriving their dependencies based on the # projects they contain. sub write_and_compare_file { my($self, $outdir, $oname, $func, @params) = @_; &$func($self, undef, @params); return 1; } sub rpmname { my($self, $mwc, $rpm2mwc, $check_unique) = @_; my $outfile = $mwc; $outfile =~ s/\.mwc$//i; $outfile = $self->get_modified_workspace_name($outfile, $ext, 1); my $base = $self->mpc_basename($outfile); $base =~ tr/-/_/; # - is special for RPM, we translate it to _ if ($check_unique && $rpm2mwc->{$base}) { die "ERROR: Can't create a duplicate RPM name: $base for mwc file $mwc\n" . "\tsee corresponding mwc file $rpm2mwc->{$base}\n"; } $rpm2mwc->{$base} = $mwc; return $base; } ## helper functions for the mini-template language sub mtl_cond { my($vars, $pre, $rep) = @_; my @v; return (@v = grep {$_} map {$rep->{lc $_}} split(' ', $vars)) ? "$pre@v" : ''; } sub mtl_apply { my($name, $subst, $rep) = @_; return join("\n", map {my $x = $subst; $x =~ s!\$_!$_!g; $x} split(' ', $rep->{lc $name})); } sub mtl_var { my($name, $default, $rep) = @_; return defined $rep->{lc $name} ? $rep->{lc $name} : (defined $default ? $default : ">>ERROR: no value for $name<<"); } ## end helper functions for the mini-template language sub post_workspace { my($self, $fh, $prjc) = @_; my $prjext = '\\' . # regexp escape for the dot that begins the extension $prjc->project_file_extension(); my %rpm2mwc; # rpm name (basename of spec file) => aggregated mwc w/ path my %mwc2rpm; # inverse of the above hash my %proj2rpm; # project name (output of mpc) => rpm name that it belongs to # first pass to build the hashes above foreach my $agg (keys %{$self->{'aggregated_mpc'}}) { my $rpm = $mwc2rpm{$agg} = $self->rpmname($agg, \%rpm2mwc, 1); foreach my $m (@{$self->{'aggregated_mpc'}->{$agg}}) { foreach my $p (@{$self->{'mpc_to_output'}->{$m}}) { $proj2rpm{$p} = $rpm; } } } if (0 == scalar keys %proj2rpm) { # nothing to generate (no aggregated workspaces) return; } my $outdir = $self->get_outdir(); my $now = strftime '%a %b %d %Y %H:%M:%S', localtime; my %assign = %{$self->get_assignment_hash()}; $assign{'rpm_description'} =~ s/\\n\s*/\n/g # Allow the description to span if exists $assign{'rpm_description'}; # multiple lines in the output map {$_ = $self->process_special($_)} values %assign; # determine when this addtemp processing should actually occur while (my($key, $arr) = each %{$self->get_addtemp()}) { foreach my $val (@$arr) { my $v = $val->[1]; $v =~ s/\\n\s*/\n/g if $key eq 'rpm_description'; $v = $self->process_special($v); $self->process_any_assignment(\%assign, $val->[0], $key, $v); } } foreach my $agg (keys %{$self->{'aggregated_mpc'}}) { my $name = "$outdir/$agg"; # $agg may contain directory parts my $dir = $self->mpc_dirname($name); my $base = $mwc2rpm{$agg}; my $rpm = $base; $rpm =~ s/$ext$//; $name = "$dir/$base"; mkpath($dir, 0, 0777) if ($dir ne '.'); my %rpm_requires; # keys are RPMs that this RPM depends on my @projects; foreach my $m (@{$self->{'aggregated_mpc'}->{$agg}}) { my $projdir = $self->mpc_dirname($m); foreach my $p (@{$self->{'mpc_to_output'}->{$m}}) { my $proj = $p; $proj =~ s/$prjext$//; push @projects, $proj; my $deps = $self->get_validated_ordering("$projdir/$p"); foreach my $d (@$deps) { my $rpmdep = $proj2rpm{$d}; if (defined $rpmdep && $rpmdep ne $base) { $rpm_requires{$rpmdep} = 1; } } } } # The hash %rep has replacement values for the template .spec file text, # those values come from a few different sources, starting with the # workspace-wide assignments, then let RPM-specific ones (from aggregated # workspaces) override those, and finally add the ones known by MPC. # process_special() handles quotes and escape characters. my %rep = %assign; while (my($key, $val) = each %{$self->{'aggregated_assign'}->{$agg}}) { $val =~ s/\\n\s*/\n/g if $key eq 'rpm_description'; $rep{$key} = $self->process_special($val); } $rep{'rpm_name'} = $rpm; $rep{'rpm_mpc_workspace'} = $self->mpc_basename($agg); $rep{'rpm_mpc_requires'} = join(' ', sort map {s/$ext$//; $_} keys %rpm_requires); my $fh = new FileHandle; open $fh, ">$name" or die "can't open $name"; my $t = get_template(); ## We have decided not to reuse the TemplateParser.pm, so this file has ## its own little template language which is a subset of that one. ## <%cond(var1 [var2...], prefix)%> ## Output the prefix text followed by the concatenated, space separated, ## values of the variables (var1, var2, etc) only if at least one of ## said values is non-empty. $t =~ s/<%cond\(([\w ]+), (.+)\)%>/mtl_cond($1, $2, \%rep)/ge; ## <%perl(expr)%> ## Evaluate an arbitrary perl expression, which can reference the normal ## variable replacements (see <%var%>, below) as $rep{'name'}. $t =~ s/<%perl\((.+)\)%>/join "\n", eval $1/ge; ## <%apply(listvar, text)%> ## Treat the value of variable 'listvar' as a list (splitting on spaces) ## and repeat the text for each element of the list, substituting $_ in ## the text with the current list element. $t =~ s/<%apply\((\w+), (.+)\)%>/mtl_apply($1, $2, \%rep)/ge; ## <%var(default)%> or <%var%> ## Output the value of variable 'var', either with a default value or an ## error if 'var' is unknown. If 'default' is enclosed in double-quotes, ## they are ignored (for compatibility with TemplateParser). $t =~ s/<%(\w+)(?:\("?([^)"]*)"?\))?%>/mtl_var($1, $2, \%rep)/ge; print $fh $t; # comment will go in the %changelog section of the .spec $self->print_workspace_comment($fh, map {$_ . "\n"} ( "* $now This file was generated by MPC.", ' Any changes made directly to this file will', ' be lost the next time it is generated.', ' MPC Command:', ' ' . $self->create_command_line_string($0, @ARGV))); close $fh; } # write the script to build .rpm files from .spec files my $fh = new FileHandle; my $name = $outdir . '/' . $self->{'workspace_name'} . '_rpm.sh'; open($fh, ">$name") or die "can't open $name"; print $fh "#!/bin/sh\n"; $self->print_workspace_comment($fh, map {$_ . "\n"} ( '# RPM creation script for MPC-generated .spec files.', "# $now", '# This file was generated by MPC. Any changes made directly to', '# this file will be lost the next time it is generated.', '# MPC Command:', '# ' . $self->create_command_line_string($0, @ARGV))); my $script = get_script(); my $temporary = $assign{'rpm_mpc_temp'}; $script =~ s!/tmp/mpcrpm!$temporary!g if defined $temporary; print $fh $script; my %seen; foreach my $project ($self->sort_dependencies($self->get_projects(), 0)) { my $rpm = $proj2rpm{$self->mpc_basename($project)}; next if !defined $rpm; if (!$seen{$rpm}) { $seen{$rpm} = 1; my $dir = $self->mpc_dirname($rpm2mwc{$rpm}); $dir = ($dir eq '.' ? '' : "$dir/"); print $fh "build $dir$rpm\n"; } } close $fh; chmod 0755, $name; } sub get_template { return <<'EOT'; License: <%rpm_license("Freeware")%> Version: <%rpm_version%> Release: <%rpm_releasenumber%> Source: <%rpm_source_base("")%><%rpm_name%>.tar.gz Name: <%rpm_name%> Group: <%rpm_group%> Summary: <%rpm_summary%> <%cond(rpm_url, URL: )%> BuildRoot: %{_tmppath}/%{name}-%{version}-root Prefix: <%rpm_prefix("/usr")%> AutoReqProv: <%rpm_autorequiresprovides("no")%> <%cond(rpm_buildrequires, BuildRequires: )%> <%cond(rpm_mpc_requires rpm_requires, Requires: )%> <%cond(rpm_provides, Provides: )%> %description <%rpm_description%> %files -f %{_tmppath}/<%rpm_name%>.flist %defattr(-,root,root) %doc %config %pre <%rpm_pre_cmd()%> %post <%rpm_post_cmd()%> %preun <%rpm_preun_cmd()%> %postun <%rpm_postun_cmd()%> %prep %setup -n <%rpm_name%>-<%rpm_version%> %build <%apply(env_check, [ -z $$_ ] && echo Environment variable $_ is required. && exit 1)%> rm -rf $RPM_BUILD_ROOT <%prebuild()%> <%makefile_generator(mwc.pl -type gnuace)%> -base install -value_project libpaths+=<%rpm_mpc_temp(/tmp/mpcrpm)%>/inst/lib -value_project includes+=<%rpm_mpc_temp(/tmp/mpcrpm)%>/inst/include <%mkgen_args()%> <%rpm_mpc_workspace%> make <%makeflags()%> %install if [ "$RPM_BUILD_ROOT" = "/" ]; then echo "Build root of / is a bad idea. Bailing." exit 1 fi rm -rf $RPM_BUILD_ROOT export staging_dir=$RPM_BUILD_ROOT/install<%rpm_prefix("/usr")%> mkdir -p $staging_dir export pkg_dir=$RPM_BUILD_ROOT/<%rpm_name%>_dir mkdir -p $RPM_BUILD_ROOT/<%rpm_name%>_dir make INSTALL_PREFIX=${staging_dir} install if [ -d ${staging_dir}/share/man ]; then files=$(find ${staging_dir}/share/man -name '*.bz2') if [[ "${files}" ]]; then echo "${files}" | xargs bunzip2 -q; fi files=$(find ${staging_dir}/share/man -name '*.[0-9]') if [[ "${files}" ]]; then echo "${files}" | xargs gzip -9; fi fi cp -a $RPM_BUILD_ROOT/install/* ${pkg_dir} find ${pkg_dir} ! -type d | sed s^${pkg_dir}^^ | sed /^\s*$/d > %{_tmppath}/<%rpm_name%>.flist find ${pkg_dir} -type d | sed s^${pkg_dir}^^ | sed '\&^/usr$&d;\&^/usr/share/man&d;\&^/usr/games$&d;\&^/lib$&d;\&^/etc$&d;\&^/boot$&d;\&^/usr/bin$&d;\&^/usr/lib$&d;\&^/usr/share$&d;\&^/var$&d;\&^/var/lib$&d;\&^/var/spool$&d;\&^/var/cache$&d;\&^/var/lock$&d;\&^/tmp/apkg&d' | sed /^\s*$/d | sed 's&^&%dir &' >> %{_tmppath}/<%rpm_name%>.flist cp -a $RPM_BUILD_ROOT/*_dir/* $RPM_BUILD_ROOT rm -rf $RPM_BUILD_ROOT/*_dir rm -rf $RPM_BUILD_ROOT/install %clean make realclean find . -name '<%makefile_name_pattern(GNUmakefile*)%>' -o -name '.depend.*' | xargs rm -f %changelog EOT } sub get_script { return <<'EOT'; RPM_TOP=`rpmbuild --showrc | grep ': _topdir\b' | sed 's/^.*: _topdir\s*//' | perl -pe's/%{getenv:(\w+)}/$ENV{$1}/g'` START_DIR=`pwd` TMP_DIR=/tmp/mpcrpm DB_DIR=`rpmbuild --showrc | grep ': _dbpath\b' | sed 's/^.*: _dbpath\s*//' | perl -pe's/%\{(\w+)\}/$x = qx(rpmbuild --showrc | grep ": $1\\\b" | sed "s\/^.*: $1\\\s*\/\/"); chomp $x; $x/e'` RPM_ARCH=${1-`uname -m`} echo MPC RPM build script: output files will be placed in $RPM_TOP/RPMS [ -z $MPC_ROOT ] && echo ERROR: MPC_ROOT must be set && exit 1 rm -rf $TMP_DIR && mkdir $TMP_DIR && cp -a $DB_DIR $TMP_DIR/db || exit $? build () { [ ! -r $1 ] && echo ERROR: File not found $1 && exit 1 PKG_DIR=`dirname $1` PKG=`basename ${1%.spec}` cd $PKG_DIR VER=`grep ^Version: $PKG.spec | sed 's/^Version: //'` REL=`grep ^Release: $PKG.spec | sed 's/^Release: //'` echo Building source .tar.gz for $PKG version $VER release $REL rm -rf $TMP_DIR/$PKG-$VER $MPC_ROOT/clone_build_tree.pl -b $TMP_DIR $PKG-$VER > /dev/null cd $TMP_DIR tar chzf $RPM_TOP/SOURCES/$PKG.tar.gz $PKG-$VER && rm -rf $PKG-$VER cp $START_DIR/$PKG_DIR/$PKG.spec $RPM_TOP/SPECS echo Running rpmbuild on $PKG.spec for arch $RPM_ARCH, see rpm-$PKG.log for details rpmbuild -ba --target $RPM_ARCH $RPM_TOP/SPECS/$PKG.spec > $START_DIR/rpm-$PKG.log 2>&1 if [ $? != 0 ]; then echo rpmbuild of $PKG.spec failed. STOPPING. exit $? fi echo Installing $PKG to the temporary area rpm --ignorearch --dbpath $TMP_DIR/db --prefix $TMP_DIR/inst -iv $RPM_TOP/RPMS/$RPM_ARCH/$PKG-$VER-$REL.$RPM_ARCH.rpm || exit $? cd $START_DIR } EOT } 1;