4 # Usage: buildtoolbox.pl toolbox-archive [config file [stage]]
9 my ($TOOLBOXFILE, # Toolbox archive to compile
10 $TOOLBOXNAME, # Name of the toolbox
11 $STAGE); # Current stage
13 # Save standard I/O for common_exec
14 open OLD_STDOUT, ">&STDOUT";
15 open OLD_STDERR, ">&STDERR";
16 open OLD_STDIN, "<&STDIN";
18 # common_log(message, type):
19 # Print a log message. Second argument is the type of the
21 # " " for a normal message
23 # ">" when starting a stage
24 # "<" when terminating a stage
25 # "$" when running a command
26 # "?" for the return code of previous command
29 my $type = shift || " ";
31 # Check message format: any newline must start by a space,
32 # no new line at end of message
33 $message =~ s/(?<=\n)(?!\s|$)/ /g;
36 print LOGFILE "[".time()."]${type}${message}\n";
37 print "[$type] $message \n";
40 # common_enter_stage(stage):
41 # Common stuff while starting a new stage
42 sub common_enter_stage {
44 common_log($STAGE, ">");
48 # Common stuff while ending new stage
49 sub common_leave_stage {
50 common_log($STAGE, "<");
53 # common_die(message):
54 # Called when a problem happens
57 common_log($message, "!");
60 while(wait() > 0) { };
65 # common_exec(command, args..., [opts]):
66 # Execute given command, places its outputs to log files. If last argument
67 # is a reference to a hash, it's considered as options for the function.
68 # Right now, only one option is available, "stderr_to_stdout", which do the
69 # same as 2>&1 in shell.
70 # Returns a file handle on STDOUT.
71 # Die if return code is non-zero or if standard error is non-empty.
74 # Human-readable form of the arguments array
85 my $refopts = pop if ref($_[-1]) eq "HASH";
87 %opts = %$refopts if defined($refopts);
89 my $cmd = join(" ", map { pretty_arg $_ } @_);
92 # Find commandnum: log files are (stage)-1.out for first
93 # command of (stage), (stage)-2.out for second command of stage,
95 $commandnum++ while(-e "$STAGE-$commandnum.out");
97 my $stdout = "$STAGE-$commandnum.out";
98 my $stderr = "$STAGE-$commandnum.err";
100 common_log("$cmd\nstdout=$stdout\nstderr=$stderr", "\$");
102 # Setup I/O for subprocess
103 open STDOUT, ">$stdout";
104 open STDERR, ">$stderr";
106 if(defined($opts{"stderr_to_stdout"})) {
108 open STDERR, ">&STDOUT";
117 open STDIN, "<&OLD_STDIN";
118 open STDOUT, ">&OLD_STDOUT";
119 open STDERR, ">&OLD_STDERR";
121 common_log("$?", "?");
122 common_die("\"$cmd\" failed (non-zero exit code)") if($? != 0);
123 common_die("\"$cmd\" failed (non-empty error output)") if(-s $stderr);
125 open my ($fd), $stdout;
130 # common_exec_scilab(script):
131 # Execute scilab script
132 sub common_exec_scilab {
134 $script = "try; $script; catch; write(%io(2), lasterror()); end; quit;";
136 my $scilab = "scilex" if($^O =~ /mswin/i);
137 $scilab = "scilab" unless(defined($scilab));
139 return common_exec($scilab, "-nwni", "-nb", "-e", $script);
143 # Return true if toolbox file extension is zip
145 return $TOOLBOXFILE =~ /\.zip$/;
149 # Get all files (names) of the compressed (in tar.gz) sources
150 sub get_tree_from_tgz {
153 my $fd = common_exec("tar", "-tf", $TOOLBOXFILE);
165 # Get all files (names) of the compressed (in zip) sources
166 sub get_tree_from_zip {
169 # tail & head are here to skip header & footer
170 my $fd = common_exec("unzip", "-l", $TOOLBOXFILE);
173 if(((/^\s*-+/)...(/^\s*-+/)) && !/^\s*-+/) { # Delete header & footer
174 # zip output format: size date time filename
175 /\s*\d+\s+\d+-\d+-\d+\s+\d+:\d+\s+(.+)/ or common_die "Bad output of unzip";
186 # Get all files (names) of the compressed sources, in a hash
187 # (hash values are meaningless, set to 1)
190 return get_tree_from_zip();
193 return get_tree_from_tgz();
197 # read_file_from_tgz(filename):
198 # Extract given file from the .zip archive
199 sub read_file_from_tgz {
200 my $filename = shift;
201 return common_exec("tar", "-xOf", $TOOLBOXFILE, "$TOOLBOXNAME/$filename");
204 # read_file_from_tgz(filename):
205 # Extract given file from the .tar.gz archive
206 sub read_file_from_zip {
207 my $filename = shift;
208 return common_exec("unzip", "-p", $TOOLBOXFILE, "$TOOLBOXNAME/$filename");
211 # read_file_from_archive(filename):
212 # Extract given file from the archive
213 sub read_file_from_archive {
215 return read_file_from_zip(@_);
218 return read_file_from_tgz(@_);
222 # read_description(*description):
223 # Check if DESCRIPTION file is correct, and parse it (return a hash
225 # First argument is a file descriptor for the DESCRIPTION file (see
227 sub read_description {
229 my @required = qw(Toolbox Version Title Author Maintainer License
230 Description ScilabVersion Category);
231 my @optional = qw(Date Depends URL Entity);
232 my (%infos, $key, $val);
233 my (%lines, %correct);
237 common_die "\":\" not followed by a space at line $." if(/:(?! )/);
238 if(/:/) { # New field
239 ($key, $val) = split(/: /, $_, 2);
244 else { # Continuation of previous field
249 # Check presence of required fields, mark them as correct
250 foreach (@required) {
251 if(!defined($infos{$_})) {
252 common_die "Mandatory field \"$_\" not defined";
259 # Mark optional fields as correct
260 foreach (@optional) {
261 if(defined($infos{$_})) {
266 # Check that there's no incorrect (= unknown) fields
267 foreach (keys(%infos)) {
268 if($correct{$_} == 0) {
269 common_die "Unknown field \"$_\" (defined at line $lines{$_})";
277 # read_description_functions(*description_functions):
278 # Parse DESCRIPTION-FUNCTIONS file (and check it, too). Like DESCRIPTION,
279 # first argument is a file descriptor. Returns a hash function name =>
280 # function description
281 sub read_description_functions {
283 my (%funcs, $func, $desc);
287 common_die "\"-\" not surrounded by spaces at line $.";
290 if(/-/) { # New field
291 ($func, $desc) = split(/ - /, $_, 2);
292 $funcs{$func} = $desc;
294 else { # Previous function description continuation
305 # Given a source tree of a toolbox (see get_tree), check if it is correct
306 # (required files are present, files are at their right place, and so on...)
311 # Check that all files are under a root which has the same name as the toolbox
312 # Delete this root to simplify other tests
313 foreach (keys %tree) {
314 if(s#^\Q$TOOLBOXNAME\E(/|$)##) {
315 $newtree{$_} = 1 if $_;
318 common_die "Incorrect archive: \"$_\" is not a child of \"$TOOLBOXNAME\"";
323 # Check that basic files are here
324 my @required = qw(DESCRIPTION DESCRIPTION-FUNCTIONS readme.txt license.txt
325 changelog.txt builder.sce);
326 push(@required, "etc/$TOOLBOXNAME.start");
327 push(@required, "etc/$TOOLBOXNAME.quit");
329 foreach (@required) {
330 if(!defined($tree{$_})) {
331 common_die "Incorrect archive: required file \"$_\" not present";
335 # macros/ must contain only .sci and .sce files
336 foreach (grep { $_ =~ m#^macros/# } keys %tree) {
337 if(!/(\.sc[ie]|\/)$/) {
338 common_die "Incorrect archive: macros/ must contain only .sci and .sce files".
343 # All fortran files must be in src/fortran
344 foreach (grep { $_ =~ /\.f$/} keys %tree) {
345 if(!m#^src/fortran/#) {
346 common_die "Incorrect archive: \"$_\" is a fortran source and hence has to be in ".
351 # All c files must be in src/c or sci_gateway/{c,fortran}
352 foreach (grep { $_ =~ /\.[ch]$/} keys %tree) {
353 if(!m#^(src/c|sci_gateway/(c|fortran))/#) {
354 common_die "Incorrect archive: \"$_\" is a C source and hence has to be in ".
355 "src/c, sci_gateway/c or sci_gateway/fortran";
359 # Constraints: if $key exists, $constraints{$key} must exist
361 qr#^help/([a-z][a-z]_[A-Z][A-Z])/[^/]+\.xml$# => sub{ "help/$1/build_help.sce" },
362 qr#^help/([a-z][a-z]_[A-Z][A-Z])/build_help.sce$# => sub{ "help/$1/addchapter.sce" },
363 qr#^help/([a-z][a-z]_[A-Z][A-Z])/addchapter.sce$# => sub{ "help/builder_help.sce" },
364 qr#^sci_gateway/builder_gateway.sce$# => sub{ "sci_gateway/loader_gateway.sce" },
365 qr#^macros/.+\.sc[ie]$# => sub{ "macros/buildmacros.sce" });
367 # Build constraints for allowed languages
372 foreach (keys %languages) {
373 # if src/(lang) has source files, src/(lang)/builder_(lang).sce must exist
374 $constraints{qr#^src/($_)/.+\.$languages{$_}$#} = sub{ "src/$1/builder_$1.sce" };
376 # if sci_gateway/(lang) has C sources, sci_gateway/(lang)/builder_gateway_(lang).sce
378 $constraints{qr#^sci_gateway/($_)/.+[ch]$#} = sub{ "sci_gateway/$1/builder_gateway_$1.sce" };
380 # if src/(lang)/builder_(lang).sce exist, src/builder_src.sce must exist
381 $constraints{qr#^src/$_/builder_$_.sce$#} = sub{ "src/builder_src.sce" };
383 # if sci_gateway/(lang)/builder_gateway_(lang).sce exist, sci_gateway/builder_gateway.sce must exist
384 $constraints{qr#^sci_gateway/$_/builder_gateway_$_.sce$#} = sub{ "sci_gateway/builder_gateway.sce" };
388 foreach my $constraint (keys %constraints) {
389 foreach my $file (keys %tree) {
390 if($file =~ $constraint) {
391 my $required = $constraints{$constraint}();
392 common_die "Invalid archive: \"$&\" needs \"$required\", which isn't in the archive"
393 unless(defined($tree{$required}));
400 # Perform basic checks
402 common_enter_stage("check");
405 common_log("Detected ZIP format");
408 common_log("Detected TAR+GZIP format");
412 common_log("Checking archive structure");
413 my %tree = get_tree();
414 common_log("Archive files:\n" . join("\n", sort keys %tree));
418 common_log("Checking DESCRIPTION");
419 my $fd = read_file_from_archive("DESCRIPTION");
420 my %desc = read_description($fd);
421 common_log("Computed DESCRIPTION:\n" .
422 join("\n", map { "$_: $desc{$_}" } sort keys %desc));
425 if($TOOLBOXNAME ne $desc{"Toolbox"}) {
426 common_die "Detected toolbox name ($TOOLBOXNAME) different from ".
427 "DESCRIPTION version ($desc{Toolbox})";
431 my $version = $1 if ($TOOLBOXFILE =~ /[^.]+\.([^-]+)/);
432 if(!defined($version)) {
433 common_die "Can't detect version from archive name ($TOOLBOXFILE)";
436 if($version ne $desc{"Version"}) {
437 common_die "Detected version ($version) different from DESCRIPTION ".
438 "version ($desc{Version})";
441 # Check DESCRIPTION-FUNCTIONS
442 common_log("Checking DESCRIPTION-FUNCTIONS");
443 $fd = read_file_from_archive("DESCRIPTION-FUNCTIONS");
444 my %funcs = read_description_functions($fd);
445 common_log("Computed DESCRIPTION-FUNCTIONS:\n" .
446 join("\n", map { "$_: $funcs{$_}" } sort keys %funcs));
448 common_leave_stage();
452 # Extract the archive
454 common_enter_stage("unpack");
457 common_exec("unzip", "-o", $TOOLBOXFILE);
460 common_exec("tar", "-xvf", $TOOLBOXFILE,
461 {'stderr_to_stdout' => 1});
464 common_leave_stage();
468 # Build up the environment
470 common_enter_stage("makeenv");
472 common_leave_stage();
476 # Returns -1 if version a < version b, 0 if equals, 1 else
477 sub compare_versions {
480 my @va = split(/\./, $versa);
481 my @vb = split(/\./, $versb);
484 return -compare_versions($versb, $versa);
487 for(my $i = 0; $i < $#vb; ++$i) {
488 return 1 if $va[$i] > $vb[$i];
489 return -1 if $va[$i] < $vb[$i];
492 return 1 if($#va > $#vb);
497 # Install toolbox dependencies
503 common_enter_stage("tbdeps");
505 # We alreay made the check, reading description should be OK
506 open $fd, "$TOOLBOXNAME/DESCRIPTION";
507 %desc = read_description($fd);
510 # Make a hash depname => depvers
511 @depsarray = split(/\s*,\s*/, $desc{"Depends"} || "");
512 foreach (@depsarray) {
513 if(/^(\S+?)\s*\(([<>]?=)\s*([^)]+)\)$/) { # toolbox-name (version-comparator version)
521 common_log("Dependencies: " . join(",", map { "$_ $deps{$_}" } keys %deps));
523 # Install dependencies
524 close(common_exec_scilab("installToolbox('$_',1,'$deps{$_}')")) foreach(keys %deps);
526 # Find toolboxes directory
527 $fd = common_exec_scilab("printf('path: %s\\n', cd(atomsToolboxDirectory()))");
531 if(/^path: (.+?)\r?$/) {
537 if(!defined($tbpath)) {
538 common_die("Can't find toolboxes directory");
541 common_log("Toolboxes directory: $tbpath\n");
543 # Check if required toolboxes are installed
544 foreach my $dep (keys %deps) {
545 common_log("Checking $dep");
546 if(! -r "$tbpath/$dep/DESCRIPTION") {
547 common_die("Needed toolbox \"$dep\" is not installed");
550 next if($deps{$dep} eq "*");
552 open $fd, "$tbpath/$dep/DESCRIPTION";
553 my %desc2 = read_description($fd);
556 $deps{$dep} =~ /^([<>]?=)(.+)$/;
558 # You can see this as "installed_version - required_version"
559 my $cmp = compare_versions($desc2{"Version"}, $2);
561 if($1 eq ">=" && $cmp == -1) { # <=> !($cmp >= 0) <=> !(installed >= required)
562 common_die("We need \"$2\" >= $1, but version $desc2{Version} installed");
564 elsif($1 eq "=" && $cmp != 0) {
565 common_die("We need \"$2\" == $1, but version $desc2{Version} installed");
567 elsif($cmp == 1) { # <=> !($cmp <= 0) <=> !(installed <= required)
568 common_die("We need \"$2\" <= $1, but version $desc2{Version} installed");
572 common_leave_stage();
576 # Install system dependencies
578 common_enter_stage("sysdeps");
580 common_leave_stage();
584 # Run the build script
586 common_enter_stage("build");
588 # Generate ccbuilder.sce (see __DATA__ section)
589 common_log("Generating ccbuilder.sce");
591 $ccbuilder .= $_ while(<DATA>);
592 open CCBUILDER, ">ccbuilder.sce";
593 print CCBUILDER $ccbuilder;
595 common_log("Generated ccbuilder.sce:\n$ccbuilder");
598 common_log("Running ccbuilder.sce");
599 my $fd = common_exec_scilab("chdir('$TOOLBOXNAME'); exec('../ccbuilder.sce')");
602 common_log("Checking build result");
606 $done = 1 if(/^atoms_cc_builder:done\r?$/);
607 if(/^atoms_cc_ilib_compile:\s*(.+?)\s*$/) {
608 common_die("Generated library \"$1\" is invalid") unless($1 && ! -d $1 && (-x $1 || $^O =~ /win/i));
612 # fixme: need to check if everything was OK in macros/help generation
614 common_die("builder.sce script didn't terminate normally") unless($done);
615 common_leave_stage();
621 common_enter_stage("pack");
623 my @files = qw(readme.txt license.txt changelog.txt DESCRIPTION-FUNCTIONS
624 DESCRIPTION macros src help sci_gateway demos tests locales includes loader.sce);
625 push(@files, "etc/$TOOLBOXNAME.start");
626 push(@files, "etc/$TOOLBOXNAME.quit");
628 my $output = $TOOLBOXFILE;
629 $output =~ s/(\.zip|\.tar.gz)$//;
632 common_log("Making binary .tar.gz archive ($output.tar.gz)");
633 common_exec("tar", "-cvf", "$output.tar.gz", (map { "$TOOLBOXNAME/$_" } @files),
634 {"stderr_to_stdout" => 1});
635 common_log("Making binary .zip archive ($output.zip)");
636 common_exec("zip", "-r", "$output.zip", map { "$TOOLBOXNAME/$_" } @files);
638 common_leave_stage();
642 # Clean up the environment
644 common_enter_stage("cleanenv");
646 common_leave_stage();
649 # Init global vars, check arguments
650 open LOGFILE, ">build.log";
654 $TOOLBOXFILE = shift;
655 if(!defined($TOOLBOXFILE)) {
656 common_die "Toolbox source file required";
659 if(! -r $TOOLBOXFILE) {
660 common_die "$TOOLBOXFILE doesn't exists or can't be read";
663 $TOOLBOXNAME = $1 if ($TOOLBOXFILE =~ /^([^.]+)/);
665 common_log "Toolbox: $TOOLBOXNAME";
666 common_log "Source file: $TOOLBOXFILE";
682 # Overwrite some scilab functions to get its return value and extra infos
687 old_ilib_compile = ilib_compile;
688 function libn = ilib_compile(lib_name,makename,files,ldflags,cflags,fflags)
689 libn = old_ilib_compile(lib_name,makename,files,ldflags,cflags,fflags);
690 mprintf("\natoms_cc_ilib_compile:%s/%s\n", pwd(), libn);
694 mprintf("\natoms_cc_builder:done\n");