Files
lix/src/lib/Hydra/Helper/Nix.pm
T
Eelco Dolstra 2755c895ff * In job inputs of type "build", allow the project and jobset names of
the input build to be specified, as well as constraints on the
  inputs of the inputs build.  For instance, you can require that a
  build has input `system = "i686-linux"'.

  This is important when one binary build serves as an input to
  another binary build.  Obviously, we shouldn't pass a build on
  i686-linux as an input to another on i686-darwin.  Hence the
  necessity for constraint.

  The constraint are currently quite limited.  What you really want to
  say is that the "system" input of the other build has to match the
  "system" input of this build.  But those require a bit more work
  since they introduce dependencies between inputs.
2009-03-20 14:50:09 +00:00

204 lines
5.2 KiB
Perl

package Hydra::Helper::Nix;
use strict;
use Exporter;
use File::Path;
use File::Basename;
our @ISA = qw(Exporter);
our @EXPORT = qw(
isValidPath queryPathInfo
getHydraPath getHydraDBPath openHydraDB
registerRoot getGCRootsDir gcRootFor
getPrimaryBuildsForReleaseSet getRelease getLatestSuccessfulRelease );
sub isValidPath {
my $path = shift;
#$SIG{CHLD} = 'DEFAULT'; # !!! work around system() failing if SIGCHLD is ignored
#return system("nix-store --check-validity $path 2> /dev/null") == 0;
# This is faster than calling nix-store, but it breaks abstraction...
return -e ("/nix/var/nix/db/info/" . basename $path);
}
sub queryPathInfo {
my $path = shift;
# !!! like above, this breaks abstraction. What we really need is
# Perl bindings for libstore :-)
open FH, "</nix/var/nix/db/info/" . basename $path
or die "cannot open info file for $path";
my $hash;
my $deriver;
my @refs = ();
while (<FH>) {
if (/^Hash: (\S+)$/) {
$hash = $1;
}
elsif (/^Deriver: (\S+)$/) {
$deriver = $1;
}
elsif (/^References: (.*)$/) {
@refs = split / /, $1;
}
}
close FH;
die unless defined $hash;
return ($hash, $deriver, \@refs);
}
sub getHydraPath {
my $dir = $ENV{"HYDRA_DATA"};
die "The HYDRA_DATA environment variable is not set!\n" unless defined $dir;
die "The HYDRA_DATA directory does not exist!\n" unless -d $dir;
return $dir;
}
sub getHydraDBPath {
my $path = getHydraPath . '/hydra.sqlite';
die "The Hydra database ($path) not exist!\n" unless -f $path;
return "dbi:SQLite:$path";
}
sub openHydraDB {
my $db = Hydra::Schema->connect(getHydraDBPath, "", "", {});
$db->storage->dbh->do("PRAGMA synchronous = OFF;");
return $db;
}
sub getGCRootsDir {
die unless defined $ENV{LOGNAME};
my $dir = "/nix/var/nix/gcroots/per-user/$ENV{LOGNAME}/hydra-roots";
mkpath $dir if !-e $dir;
return $dir;
}
sub gcRootFor {
my ($path) = @_;
return getGCRootsDir . basename $path;
}
sub registerRoot {
my ($path) = @_;
my $link = gcRootFor $path;
if (!-l $link) {
symlink($path, $link)
or die "cannot create GC root `$link' to `$path'";
}
}
sub attrsToSQL {
my ($attrs, $id) = @_;
my @attrs = split / /, $attrs;
my $query = "1 = 1";
foreach my $attr (@attrs) {
$attr =~ /^([\w-]+)=([\w-]*)$/ or die "invalid attribute in release set: $attr";
my $name = $1;
my $value = $2;
# !!! Yes, this is horribly injection-prone... (though
# name/value are filtered above). Should use SQL::Abstract,
# but it can't deal with subqueries. At least we should use
# placeholders.
$query .= " and exists (select 1 from buildinputs where build = $id and name = '$name' and value = '$value')";
}
return $query;
}
sub getPrimaryBuildsForReleaseSet {
my ($project, $primaryJob) = @_;
my @primaryBuilds = $project->builds->search(
{ jobset => $primaryJob->get_column('jobset'), job => $primaryJob->get_column('job'), finished => 1 },
{ join => 'resultInfo', order_by => "timestamp DESC"
, '+select' => ["resultInfo.releasename"], '+as' => ["releasename"]
, where => \ attrsToSQL($primaryJob->attrs, "me.id")
});
return @primaryBuilds;
}
sub getRelease {
my ($primaryBuild, $jobs) = @_;
my @jobs = ();
my $status = 0; # = okay
# The timestamp of the release is the highest timestamp of all
# constitutent builds.
my $timestamp = 0;
foreach my $job (@{$jobs}) {
my $thisBuild;
if ($job->isprimary) {
$thisBuild = $primaryBuild;
} else {
# Find a build of this job that had the primary build
# as input. If there are multiple, prefer successful
# ones, and then oldest. !!! order_by buildstatus is hacky
($thisBuild) = $primaryBuild->dependentBuilds->search(
{ jobset => $job->get_column('jobset'), job => $job->get_column('job'), finished => 1 },
{ join => 'resultInfo', rows => 1
, order_by => ["buildstatus", "timestamp"]
, where => \ attrsToSQL($job->attrs, "build.id")
});
}
if ($job->mayfail != 1) {
if (!defined $thisBuild) {
$status = 2 if $status == 0; # = unfinished
} elsif ($thisBuild->resultInfo->buildstatus != 0) {
$status = 1; # = failed
}
}
$timestamp = $thisBuild->timestamp
if defined $thisBuild && $thisBuild->timestamp > $timestamp;
push @jobs, { build => $thisBuild, job => $job };
}
return
{ id => $primaryBuild->id
, releasename => $primaryBuild->get_column('releasename')
, jobs => [@jobs]
, status => $status
, timestamp => $timestamp
};
}
sub getLatestSuccessfulRelease {
my ($project, $primaryJob, $jobs) = @_;
my $latest;
foreach my $build (getPrimaryBuildsForReleaseSet($project, $primaryJob)) {
return $build if getRelease($build, $jobs)->{status} == 0;
}
return undef;
}
1;