#!/usr/bin/perl # # Copyright 2011 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # # A simple little build system. # # See the configuration at the bottom of this file. use strict; use Getopt::Long; use FindBin; my $opt_list; my $opt_eachclean; my $opt_verbose; my $opt_test; my $opt_deps; chdir($FindBin::Bin) or die "Couldn't chdir to $FindBin::Bin: $!"; GetOptions("list" => \$opt_list, "eachclean" => \$opt_eachclean, "verbose+" => \$opt_verbose, "test" => \$opt_test, "deps" => \$opt_deps, ) or usage(); sub usage { die < bool (was it already built?) my %targets; # dir -> { deps => [ ... ], } read_targets(); if ($opt_list) { print "Available targets:\n"; foreach my $target (sort keys %targets) { print " * $target\n"; } exit; } if ($opt_deps) { foreach my $target (sort keys %targets) { find_go_camli_deps($target); my $t = $targets{$target} or die; print "$target: @{ $t->{deps} }\n"; } exit; } if ($opt_eachclean) { my $target = shift; die "--eachclean doesn't take a target\n" if $target; foreach my $target (sort keys %targets) { clean(); %built = (); build($target); } exit; } my $target = shift or usage(); if ($target eq "clean") { clean(); exit; } if ($target eq "allfast") { foreach my $target (sort grep { !$targets{$_}{tags}{not_in_all} } keys %targets) { build($target); } exit; } if ($target eq "all") { foreach my $target (sort keys %targets) { build($target); } exit; } my @matches = grep { /$target/ } sort keys %targets; unless (@matches) { die "No build targets patching pattern /$target/\n"; } if (@matches > 1) { if (scalar(grep { $_ eq $target } @matches) == 1) { @matches = ($target); } else { die "Build pattern is ambiguous, matches multiple targets:\n * " . join("\n * ", @matches) . "\n"; } } build($matches[0]); sub v { return unless $opt_verbose; my $msg = shift; chomp $msg; print STDERR "# $msg\n"; } sub v2 { return unless $opt_verbose >= 2; my $msg = shift; chomp $msg; print STDERR "# $msg\n"; } sub clean { for my $root ("$ENV{GOROOT}/pkg/linux_amd64", "$ENV{GOROOT}/pkg/linux_386") { for my $pkg ("camli") { my $dir = "$root/$pkg"; next unless -d $dir; system("rm", "-rfv", $dir); } } foreach my $target (sort keys %targets) { print STDERR "Cleaning $target\n"; system("make", "-C", $target, "clean"); } } # Returns a help message on a build failure of a given target. sub fixit_tip { my $target = shift; if ($target =~ /\bgo\b/) { my $gover = `gotry runtime 'Version()'`; unless ($gover =~ /Version.+"(.+)"/) { return "Failed to find 'gotry'. Is Go installed? Or have you put \$GOROOT/bin in your \$PATH?"; } $gover = $1; if ($gover =~ /release/) { return "You're running a release version of Go ($gover) but \n". "Camlistore generally tracks the 'weekly' releases.\n". "See: http://blog.golang.org/2011/03/go-becomes-more-stable.html\n"; } unless ($gover =~ /weekly\.(\d\d\d\d)-(\d\d)-(\d\d)/) { return "Failed to parse your Go version. You have \"$gover\" but since\n". "I can't parse it, I can't tell you if it's too old or not.\n"; } my ($yyyy, $mm, $dd) = ($1, $2, $3); # TODO: check the internet to see what the latest Go weekly is? # Or keep it in git here? Or go purely on number of days passed? return "You're running Go weekly release $gover; maybe it's too old?"; } if ($target =~ /\bandroid\b/) { return "Have you installed the Android SDK, installed ant, set \$JAVA_HOME?\n". "Unset \$JAVA_HOME if it points to a bogus place? Run update-java-alternatives?\n". "See errors above."; } return ""; } my $did_go_check = 0; sub perform_go_check() { return if $did_go_check++; if ($ENV{GOROOT}) { die "Your \$GOROOT environment variable isn't a directory.\n" unless -d $ENV{GOROOT}; return 1; } # No GOROOT set; see if they have 8g or 6g if (`which 8g` =~ /\S/ || `which 6g` =~ /\S/) { die "You seem to have Go installed, but you don't have your ". "\$GOROOT environment variable set.\n". "Can't build without it.\n"; } die "You don't seem to have Go installed. See:\n\n http://golang.org/doc/install.html\n\n"; } sub build { my @history = @_; my $target = $history[0]; if ($target =~ m!/go/!) { perform_go_check(); } my $already_built = $built{$target} || 0; v2("Building '$target' (already_built=$already_built; via @history)"); return if $already_built; $built{$target} = 1; if ($target =~ /^ext:(.+)/) { die "\$GOROOT not set" unless $ENV{GOROOT} && -d $ENV{GOROOT}; my $golib = $1; my @files = glob("$ENV{GOROOT}/pkg/*/${golib}.a"); unless (@files) { die "You need to run:\n\n\tgoinstall $golib\n"; } return; } my $t = $targets{$target} or die "Bogus or undeclared build target: $target\n"; # Add auto-learned dependencies. find_go_camli_deps($target); gen_target_makefile($target); # Dependencies first. my @deps = @{ $t->{deps} }; if (@deps) { v2("Deps of '$target' are @deps"); foreach my $dep (@deps) { build($dep, @history); } v2("built deps for $target, now can build"); } my @quiet = ("--silent"); @quiet = () if $opt_verbose; if (system("make", @quiet, "-C", $target, "install") != 0) { my $chain = ""; if (@history > 1) { $chain = "(via chain @history)"; } my $help_msg = fixit_tip($target); if ($help_msg) { $help_msg = "\nPossible tip: $help_msg\n\n"; } my $deps; if ($chain) { $deps = " (via deps: $chain)"; } die "\nError building $target$deps\n$help_msg"; } v("Built '$target'"); if ($opt_test && !$t->{tags}{skip_tests}) { opendir(my $dh, $target) or die; my @test_files = grep { /_test\.go$/ } readdir($dh); closedir($dh); if (@test_files) { if (system("make", @quiet, "-C", $target, "test") != 0) { die "Tests failed for $target\n"; } } } } sub find_go_camli_deps { my $target = shift; if ($target =~ /\bthird_party\b/) { # Skip third-party stuff. return; } unless ($target =~ m!lib/go/camli! || $target =~ m!(server|clients)/go\b!) { return; } my $t = $targets{$target} or die "Bogus or undeclared build target: $target\n"; opendir(my $dh, $target) or die "Failed to open directory: $target\n"; my @go_files = grep { !m!^\.\#! } grep { !/_testmain\.go$/ } grep { /\.go$/ } readdir($dh); closedir($dh); # TODO: just stat the files first and keep a cache file of the # deps somewhere (the header of the generated Makefile?) but # maybe it's not worth it. for now we'll parse all the files # every time to find their deps and also generate the Makefile # every time. my @deps; my %seen; # $dep -> 1 for my $f (@go_files) { open(my $fh, "$target/$f") or die "Failed to open $target/$f: $!"; my $src = do { local $/; <$fh>; }; unless ($src =~ m!\bimport\s*\((.+?)\)!s) { die "Failed to parse imports from $target/$f.\n". "No imports(...) block? Um, add a fake one. :)\n"; } my $imports = $1; while ($imports =~ m!"(camli\b.+?)"!g) { my $dep = "lib/go/$1"; push @deps, $dep unless $seen{$dep}++; } } foreach my $dep (@deps) { unless (grep { $_ eq $dep } @{$t->{deps}}) { push @{$t->{deps}}, $dep; } } } sub gen_target_makefile { my $target = shift; my $type = ""; if ($target =~ m!lib/go/camli!) { $type = "pkg"; } elsif ($target =~ m!(server|clients)/go\b!) { $type = "cmd"; } else { return; } my $t = $targets{$target} or die "Bogus or undeclared build target: $target\n"; my @deps = @{$t->{deps}}; opendir(my $dh, $target) or die; my @go_files = grep { !m!^\.\#! } grep { !/_testmain\.go$/ } grep { /\.go$/ } readdir($dh); closedir($dh); # Generate the Makefile my $mfc = "\n\n"; $mfc .= "###### NOTE: THIS IS AUTO-GENERATED FROM build.pl IN THE ROOT; DON'T EDIT\n"; $mfc .= "\n\n"; $mfc .= "include \$(GOROOT)/src/Make.inc\n"; if (@deps) { my $pr = ""; foreach my $dep (@deps) { my $cam_lib = $dep; $cam_lib =~ s!^lib/go/!!; $pr .= '$(QUOTED_GOROOT)/pkg/$(GOOS)_$(GOARCH)/' . $cam_lib . ".a\\\n\t"; } chop $pr; chop $pr; chop $pr; $mfc .= "PREREQ=$pr\n"; } if ($type eq "pkg") { my $targ = $target; $targ =~ s!^lib/go/!!; $mfc .= "TARG=$targ\n"; } else { my $targ = $target; $targ =~ s!^.*/!!; $mfc .= "TARG=$targ\n"; } my @non_test_files = grep { !/_test\.go/ } @go_files; $mfc .= "GOFILES=@non_test_files\n"; $mfc .= "include \$(GOROOT)/src/Make.$type\n"; set_file_contents("$target/Makefile", $mfc); # print "DEPS of $target: @{ $t->{deps} }\n"; } sub set_file_contents { my ($fn, $new) = @_; if (-e $fn) { open(my $fh, $fn) or die "Failed to write to $fn: $!"; my $cur = do { local $/; <$fh> }; return if $new eq $cur; } open(my $fh, ">$fn") or die; print $fh $new; close($fh) or die; } sub read_targets { my $last; for () { if (m!^\TARGET:\s*(.+)\s*$!) { my $target = $1; $last = $target; $targets{$target} ||= { deps => [] }; next; } s/\#.*//; if (m!^\s+\-\s(\S+)\s*$!) { my $dep = $1; my $t = $targets{$last} or die "Unexpected dependency line: $_"; push @{$t->{deps}}, $dep; next; } if (m!^\s+\=\s*(\S+)\s*$!) { my $tag = $1; my $t = $targets{$last} or die "Unexpected dependency line: $_"; $t->{tags}{$tag} = 1; next; } } #use Data::Dumper; #print Dumper(\%targets); } __DATA__ TARGET: clients/go/camdbinit TARGET: clients/go/camget TARGET: clients/go/camput TARGET: clients/go/cammount TARGET: clients/go/camsync TARGET: lib/go/camli/auth TARGET: lib/go/camli/blobref TARGET: lib/go/camli/blobserver TARGET: lib/go/camli/blobserver/handlers TARGET: lib/go/camli/blobserver/localdisk TARGET: lib/go/camli/blobserver/remote TARGET: lib/go/camli/blobserver/s3 TARGET: lib/go/camli/client TARGET: lib/go/camli/errorutil TARGET: lib/go/camli/httputil TARGET: lib/go/camli/jsonconfig TARGET: lib/go/camli/jsonsign TARGET: lib/go/camli/lru TARGET: lib/go/camli/magic TARGET: lib/go/camli/misc TARGET: lib/go/camli/misc/amazon/s3 TARGET: lib/go/camli/misc/httprange TARGET: lib/go/camli/misc/gpgagent TARGET: lib/go/camli/misc/pinentry TARGET: lib/go/camli/mysqlindexer TARGET: lib/go/camli/netutil TARGET: lib/go/camli/osutil TARGET: lib/go/camli/schema TARGET: lib/go/camli/search TARGET: lib/go/camli/test TARGET: lib/go/camli/test/asserts TARGET: lib/go/camli/third_party/github.com/hanwen/go-fuse/fuse =skip_tests TARGET: lib/go/camli/third_party/github.com/Philio/GoMySQL =skip_tests TARGET: lib/go/camli/webserver TARGET: server/go/camlistored TARGET: server/go/sigserver TARGET: website TARGET: clients/android =not_in_all # too slow