# Hydra build file for coccinelle

{ nixpkgs ? "/etc/nixos/nixpkgs"
, cocciSrc ? { outPath = ./.; revCount = 1234; gitTag = "abcdef"; }
, testsSrc ? { outPath = ../big-tests; rev = 1234; }
, officialRelease ? false
, performRegress ? true
}:


let
  
  # version information
  version = builtins.readFile ./version;
  versionSuffix = if officialRelease then "" else "pre${toString cocciSrc.revCount}-${cocciSrc.gitTag}";


  #
  # Source release (tarball)
  #

  # The source tarball taken from the repository.
  # The tarball should actually be compilable using
  #   ./configure && make depend && make opt && make install
  # on systems other than nix.
  tarball =
    let pkgs = import nixpkgs { };
    in with pkgs; with ocamlPackages; releaseTools.sourceTarball {
      name = "coccinelle-tarball";
      src = cocciSrc;
      inherit officialRelease;
      inherit version;
      inherit versionSuffix;

      buildInputs = [
        ocaml findlib menhir python
        texLiveFull # for building the documentation
	pkgconfig  # for the autoconf macros
      ];

      preDist = ''
        local PREVHOME=$HOME
        export HOME=$TMPDIR    # the latex installation needs to write to the $HOME directory, so rename it here
      '';
      
      dontCopyDist = 1; # we'll copy the tarball to the tarballs folder ourselves (and rename it)
      postDist = ''
        export HOME=$PREVHOME  # restore the home directory

        mkdir -p "$out/tarballs"

        # rename the tarball to give it a version-specific name
        cp coccinelle-*.tar.gz "$out/tarballs/coccinelle-${version}${versionSuffix}.tar.gz"
      '';
    };


  #
  # Helper functions for building configurations
  #

  selOcamlDefault = orig: orig.ocamlPackages;
  selOcaml400 = orig: orig.ocamlPackages_4_00_0;
  selOcaml312 = orig: orig.ocamlPackages_3_12_1;
  selOcaml311 = orig: orig.ocamlPackages_3_11_2;
  selOcaml310 = orig: orig.ocamlPackages_3_10_0;

  selCommonOcamlPkgs = ocamlPackages: with ocamlPackages; [
    findlib menhir ocaml_sexplib
  ];

  selMinimalOcamlPkgs = ocamlPackages: with ocamlPackages; [
    findlib menhir
  ];

  selAllOcamlPkgs = ocamlPackages: with ocamlPackages; [
    findlib menhir ocaml_sexplib ocaml_pcre pycaml
  ];

  selCommonInputs = pkgs: [ pkgs.pkgconfig pkgs.pcre ];

  selDefaultShell = pkgs: pkgs.stdenv.shell;

  selPythonNone = pkgs: [];
  selPythonDefault = pkgs: [ pkgs.python ];
  selPython2 = pkgs: [ pkgs.python27 ];
  selPython3 = pkgs: [ pkgs.python3 ];

  # creates a configuration for a given python version
  mkCfgPython = f: pkgs: with (f pkgs); {
    inherit name pythons flags;

    ocamls = selCommonOcamlPkgs pkgs.ocamlPackages ++ [ pkgs.ocamlPackages.pycaml ];
    selOcaml = selOcamlDefault;
    extras = selCommonInputs pkgs;
    shell = selDefaultShell pkgs;
    extraAttrs = { };
  };

  # creates a configuration for a given ocaml version
  mkCfgOcaml = { name, selOcaml, flags }: pkgs: {
      inherit flags selOcaml;

    name = "ocaml-${name}";
    pythons = selPythonDefault pkgs;
    ocamls = selMinimalOcamlPkgs pkgs.ocamlPackages;
    extras = selCommonInputs pkgs;
    shell = selDefaultShell pkgs;
    extraAttrs = { };
  };

  # creates a default configuration with additional flags
  mkCfgDefault = { name, flags, extra ? {} }: pkgs: {
    inherit name flags;
    pythons = selPythonDefault pkgs;
    ocamls = selAllOcamlPkgs pkgs.ocamlPackages;
    selOcaml = selOcamlDefault;
    extras = selCommonInputs pkgs;
    shell = selDefaultShell pkgs;
    extraAttrs = extra;
  };

  # creates a minimal configuration with additional flags
  mkCfgMinimal = { name, flags }: pkgs: {
    inherit name flags;
    pythons = [];
    ocamls = [];
    selOcaml = selOcamlDefault;
    extras = [];
    shell = selDefaultShell pkgs;
    extraAttrs = { };
  };

  # creates a configuration for the given ocaml packages
  mkCfgPackage = { name, ocamls, flags }: pkgs: {
    inherit name flags;
    pythons = selPythonDefault pkgs;
    ocamls = selMinimalOcamlPkgs pkgs.ocamlPackages ++ ocamls pkgs.ocamlPackages;
    selOcaml = selOcamlDefault;
    extras = selCommonInputs pkgs;
    shell = selDefaultShell pkgs;
    extraAttrs = { };
  };

  # build the project using the given shell
  # it takes a minimal configuration, but then with all the
  # libraries that trigger features of coccinelle to be enabled.
  mkCfgShell = { name, selShell }: pkgs: {
    inherit name;
    pythons = selPythonDefault pkgs;
    ocamls = selMinimalOcamlPkgs pkgs.ocamlPackages;
    selOcaml = selOcamlDefault;
    flags = [];
    extras = [ pkgs.pcre ];
    shell = selShell pkgs;
    extraAttrs = { };
  };

  # creates a configuration with multiple ocaml versions: this gives
  # conflicts. This is just a test to see whether our build system is
  # not too much confused in this case. It seems at least that ocamlfind
  # cannot be used in this setting.
  mkCfgManyOcaml =
    let
      selOcaml = pkgs: ocamlPkgSel: with (ocamlPkgSel pkgs); ocaml;
      selPkgs = pkgs: ocamlPkgSel: with (ocamlPkgSel pkgs); [ menhir ];
    in sels: pkgs: {
      name = "many-ocaml";
      pythons = [];
      ocamls = pkgs.lib.concatMap (selPkgs pkgs) sels;
      selOcaml = selOcamlDefault;
      flags = [];
      extras = selCommonInputs pkgs ++ map (selOcaml pkgs) sels;
      shell = selDefaultShell pkgs;
      extraAttrs = { };
    };


  #
  # Configurations
  #

  defaultCfg = mkCfgDefault { name = "default"; flags = []; };
  debugCfg = mkCfgDefault { name = "debug"; flags = [ "--enable-release=no" ]; };
  wrappersCfg = mkCfgDefault { name = "wrappers"; flags = [ "--enable-python" "--enable-ocaml" "--without-pkg-config" "--without-ocamlfind" ]; };
  manyOcamlCfg = mkCfgManyOcaml [ selOcaml400 selOcaml312 selOcaml311 selOcaml310 ];

  minimalCfgs = map mkCfgMinimal [
    { name = "minimal"; flags = []; }
    { name = "noocamlscripting"; flags = [ "--disable-ocaml" ]; }
  ];

  # Several configurations testing different python versions.
  # We exlicitly pass the "--enable-python" flag so that the
  # build should fail if no suitable python can be detected.
  pythonCfgs = 
    map mkCfgPython [
      ( _ : { name = "no-python"; pythons = []; flags = []; })

      (pkgs: {
        name = "python2-local";
        pythons = selPython2 pkgs;
        flags = [ "--enable-python" "--disable-pycaml" ];
      })

      (pkgs: {
        name = "python3-local";
        pythons = selPython3 pkgs;
        flags = [ "--enable-python" "--disable-pycaml" ];
      })

      (pkgs: {
        name = "python3-global";
        pythons = selPython3 pkgs;
        flags = [ "--enable-python" ];
      })

      (pkgs: {
        name = "python-nopkgconfig";
        pythons = selPython2 pkgs;
        flags = [ "--enable-python" "--without-pkg-config" ];
      })

#  disabled because this combination does not work in NixOS
#      (pkgs: {
#        name = "many-pythons";
#        pythons = selPython3 pkgs ++ selPython2 pkgs;
#        flags = [ "--with-python=python3" ];
#      })
    ];

  # Several configurations testing different OCaml versions.
  # These versions ship with minimal global packages in order
  # to thest the bundled packages with these ocaml versions.
  ocamlCfgs = map mkCfgOcaml [
    { name = "400nat"; selOcaml = selOcaml400; flags = [ "--enable-release=yes" ]; }
    { name = "400byt"; selOcaml = selOcaml400; flags = []; }
    { name = "312"; selOcaml = selOcaml312; flags = []; }
    { name = "311"; selOcaml = selOcaml311; flags = [ "--enable-release=yes" ]; }
    { name = "310"; selOcaml = selOcaml310; flags = []; }
  ];

  # Several configurations testing different available
  # ocaml packages.
  pkgCfgs = map mkCfgPackage [
    { name = "pcre"; ocamls = ps: [ ps.ocaml_pcre ]; flags = [ "--enable-pcre-syntax" ]; }
    { name = "sexplib"; ocamls = ps: [ ps.ocaml_sexplib ]; flags = [ "--enable-sexplib" ]; }
    { name = "pycaml"; ocamls = ps: [ ps.pycaml ]; flags = [ "--enable-pycaml" ]; }
  ];

  # Tests using several different types of shells.
  shellCfgs = map mkCfgShell [
    { name = "bash"; selShell = pkgs: "${pkgs.bash}/bin/bash"; }
    { name = "dash"; selShell = pkgs: "${pkgs.dash}/bin/dash"; }
    { name = "zsh"; selShell = pkgs: "${pkgs.zsh}/bin/zsh"; }

    # the configure script is not compatible with tcsh
    # { name = "tcsh"; selShell = pkgs: "${pkgs.tcsh}/bin/tcsh"; }
  ];

  #
  # Configurations for the compilation of coccinelle using ocamlbuild.
  #

  ocamlbuildZeroCfg = mkCfgMinimal {
    name  = "ocamlbuild-zero";
    flags = [ "--enable-ocamlbuild" "--enable-release" ];
  };

  ocamlbuildFullCfg = mkCfgDefault {
    name  = "ocamlbuild-full";
    flags = [ "--enable-ocamlbuild" "--enable-release" ];
  };

  ocamlbuildCfgs = map mkCfgOcaml [
    { name = "ocamlbuild-400nat"; selOcaml = selOcaml400;
      flags = [ "--enable-ocamlbuild" "--enable-release=yes" ]; }
    { name = "ocamlbuild-400byte"; selOcaml = selOcaml400;
      flags = [ "--enable-ocamlbuild" ]; }
    { name = "ocamlbuild-312"; selOcaml = selOcaml312;
      flags = [ "--enable-ocamlbuild" "--enable-release" ]; }
    { name = "ocamlbuild-311"; selOcaml = selOcaml311;
      flags = [ "--enable-ocamlbuild" ]; }
    { name = "ocamlbuild-310"; selOcaml = selOcaml310;
      flags = [ "--enable-ocamlbuild" "--enable-release" ]; }
  ] ++ [ ocamlbuildZeroCfg ocamlbuildFullCfg ];

  altCfgs =
    [ debugCfg manyOcamlCfg ]
    ++ minimalCfgs
    ++ ocamlCfgs ++ pythonCfgs
    ++ pkgCfgs ++ shellCfgs
    ++ ocamlbuildCfgs;


  #
  # Builds for specific configurations
  #

  # builds coccinelle, parameterized over the ocaml and python packages, and the configure flags.
  # the result should be a usable nix-expression

  # mkConfiguration is a function that takes the nix package collection of the build
  # (called 'pkgs') and results in a record containing:
  # name of the configuration, python packages, ocaml packages selection function
  # (which takes the original 'pkgs' as parameter), and ocaml packages. The selection
  # function is used by 'mkConfiguration' to determine the appropriate ocamlPackages
  # field in 'pkgs'.
  mkBuild = mkConfiguration: { system ? builtins.currentSystem }:
    let pkgs = import nixpkgs {
          inherit system;
          config.packageOverrides = orig : {
            ocamlPackages = cfg.selOcaml orig;
          };
        };
        cfg = mkConfiguration pkgs;
        flags = [ "--enable-release=world" ] ++ cfg.flags;
    in with pkgs; releaseTools.nixBuild ({
      inherit (cfg) shell;
      name = "cocci-build-${cfg.name}";
      src = tarball;
      enableParallelBuilding = true;
      buildInputs = cfg.extras ++ [ ncurses ocamlPackages.ocaml ] ++ cfg.ocamls ++ cfg.pythons;
      configureFlags = pkgs.lib.concatStringsSep " " flags; # hmm, flags are now not allowed to contain spaces
      doCheck = true;
      
      buildPhase = ''
        mkdir -p "$out/nix-support/"
        touch "$out/nix-support/make.log"
        echo "report log $out/nix-support/result.log" >> "$out/nix-support/hydra-build-products"

        make all 2> >(tee -a "$out/nix-support/make.log" >&2)
      '';

      # changes the shell in some of the scripts to the configured one
      prePatch = ''
        echo "patching the shell in scripts to: ${cfg.shell}"
        for script in configure scripts/spatch.sh.in scripts/genversion.sh \
          setup/fake-subst.sh setup/fake-menhir.sh setup/fake-pdflatex.sh; do
          substituteInPlace $script --replace '#! /bin/sh' '#! ${cfg.shell}'
        done
      '';
    } // cfg.extraAttrs);

  build = mkBuild defaultCfg;
  altBuilds = map mkBuild altCfgs;
  allBuilds = [ build ] ++ altBuilds;

  # compile with ocaml profiling turned on and then running the
  # test suite to collect results.
  profileCfg = mkCfgDefault {
    name = "profiling";
    flags = [ "--enable-release=profile" ];
    extra = {
      installPhase = ''
        mkdir -p "$out/nix-support"
        cp ocamlprof.dump "$out/ocamlprof.dump"
        echo "file binary $out/ocamlprof.dump" >> "$out/nix-support/hydra-build-products"
      '';
    };
  };
  profile = mkBuild profileCfg {};


  #
  # Package builders
  #

  # package builder for Debian-based OS'ses
  makeDeb =
    system: diskImageFun:
    
    with import nixpkgs { inherit system; };
    releaseTools.debBuild {
      name = "coccinelle-deb";
      src = tarball;
      diskImage = diskImageFun vmTools.diskImageFuns {
        extraPackages = [ "python" "python-support" "ocaml-nox" "ocaml-findlib" ];
      };
      debRequires = [ "python" "python-support" "ocaml-nox" "ocaml-findlib" ];
      doCheck = false;

      buildPhase = ''
        make depend
        make all
        make all.opt
      '';
    };

  makeDeb_i686 = makeDeb "i686-linux";
  makeDeb_x86_64 = makeDeb "x86_64-linux";

  # different debian builds
  # deb_ubuntu1010_i386 = makeDeb_i686 (disk: disk.ubuntu1010i386);
  # deb_ubuntu1010_x86_64 = makeDeb_x86_64 (disk: disk.ubuntu1010x86_64);


  #
  # Testing tasks
  #

  mkTask =
    argsfun: { system ? builtins.currentSystem }:
    let pkgs = import nixpkgs { inherit system; };
        args = argsfun pkgs system;
        name = "${args.name}-${version}${versionSuffix}";
    in pkgs.stdenv.mkDerivation ({
      phases = [ "runPhase" ];

      runPhase = ''
        mkdir -p "$out"
        mkdir -p "$out/nix-support"
        touch "$TMPDIR/result.log"
        exec > >(tee -a "$TMPDIR/result.log") 2> >(tee -a "$TMPDIR/result.log" >&2)
        runHook execPhase
        cp "$TMPDIR/result.log" "$out/"
        echo "report log $out/result.log" >> "$out/nix-support/hydra-build-products"
        echo "$name" > "$out/nix-support/hydra-release-name"
      '';

      meta = {
        description = "Coccinelle post-build task";
        schedulingPriority = 8;
      };
    } // args // { inherit name; });

  mkReport = inputs: mkTask (pkgs: _: with pkgs;
    let builds = map (i: i { inherit (pkgs.stdenv) system; }) inputs; in {
      name = "report";

      execPhase = ''
        echo "collecting logs"
        for build in ${lib.concatStringsSep " " builds}; do
          echo "log: $build/nix-support/make.log"
          cat "$build/nix-support/make.log"
        done

        echo "grepping OCaml warnings"
        if grep -2 "Warning " "$TMPDIR/result.log"
        then
          echo "found warnings!"
          false
        else
          echo "there are apparently no significant warnings"
        fi
      '';

      meta = {
        description = "Analysis of the coccinelle build reports";
        schedulingPriority = 5;
      };
    });

  report = mkReport allBuilds;


  #
  # Regression tests
  #

  # Produces regression test results, which can be positive or
  # negative. The build should succeed regardless of the outcome
  # of individual tests unless coccinelle is horribly broken.
  # The resulting files are stored in a tarball so that it allows
  # manual inspection.
  mkRegress = cocciSelect: mkTask (pkgs: system: with pkgs;
    let coccinelle = cocciSelect { inherit system; };
    in {
      name = "regression-${toString testsSrc.rev}";
      buildInputs = [ coccinelle ];

      execPhase = ''
        # prepare a writeable tests directory
        # as this directory contains large
        # files, we'll create links to the
        # individual files.
        mkdir -p "$TMPDIR/tests"
        cp -rs ${testsSrc}/* "$TMPDIR/tests/"
	chmod -R u+w "$TMPDIR/tests/"
        cd "$TMPDIR/tests"

	# initialize essential environment variables
        # for the makefile
	export COCCINELLE_HOME=${coccinelle}/share/coccinelle
        export COCCIDIR=$TMPDIR
        export SPATCH=${coccinelle}/bin/spatch.opt
        export ISO=${coccinelle}/share/coccinelle/standard.iso
        export DEFS=${coccinelle}/share/coccinelle/standard.h

	# generate the test outcomes using a parallel build
        make -e all -j$NIX_BUILD_CORES -l$NIX_BUILD_CORES

        # collect the results
	# note: the tarball is likely to contain useless
        # symbolic links to files in the nix store. We therefore
        # delete these symlinks. As a result, you should be able
        # to unpack the tarball in the tests directory.
        find "$TMPDIR/tests" -depth -type l -delete
        cd "$TMPDIR"
        tar -czf "$out/results.tar.gz" ./tests
	echo "file binary-dist $out/results.tar.gz" >> "$out/nix-support/hydra-build-products"
      '';

      meta = {
        description = "Regression test of Coccinelle";
        schedulingPriority = 8;
      };
    });

  # Checks whether the regression tests meet our expectations.
  # If the set of failed tests is different than specified in
  # the tests repository, this check fails.
  checkRegress = regressSelect: mkTask (pkgs: system: with pkgs;
    let regress = regressSelect { inherit system; };
    in {
      name = "test-${toString testsSrc.rev}";

      execPhase = ''
        # prepare a writeable tests directory
        # as this directory contains large
        # files, we'll create links to the
        # individual files.
        mkdir -p "$TMPDIR/tests"
        cp -rs ${testsSrc}/* "$TMPDIR/tests/"
	chmod -R u+w "$TMPDIR/tests/"

        # extract the outcome of the regression test over it
	echo "reconstructing regression directory"
        cd "$TMPDIR"
        tar xfz "${regress}/results.tar.gz"
        cd "$TMPDIR/tests"

	echo "analyzing results"
	make failedlog

	echo "verifying the outcome"
	make check
      '';

      meta = {
        description = "Regression test of Coccinelle";
        schedulingPriority = 8;
      };
    });

  regress = mkRegress build;
  test = checkRegress regress;


  #
  # Performing release actions
  #

  dist =
    let pkgs = import nixpkgs { };
        name = "release-${version}${versionSuffix}";
    in with pkgs; releaseTools.nixBuild {
      inherit name;
      src = cocciSrc;
      buildInputs = with ocamlPackages; [
        pkgconfig ncurses texLiveFull
        ocaml findlib menhir
        python pcre patchelf
      ];
      configureFlags = "--enable-release";

      buildPhase = ''
        export TARGETDIR="$TMPDIR/dists"
        mkdir -p $TARGETDIR
        export HOME=$TMPDIR
	make prerelease GIT=echo TMP=$TARGETDIR
	make release GIT=echo TMP=$TARGETDIR
	make package TMP=$TARGETDIR
      '';

      installPhase = ''
        mkdir -p "$out/nix-support/"
	echo "cocci-dist-${version}" > "$out/nix-support/hydra-release-name"
	cp $TMPDIR/dists/*.tgz "$out/"
	for file in $out/*.tgz; do
          echo "file binary-dist $file" >> $out/nix-support/hydra-build-products
	done
      '';

      dontInstall = false;
      doCheck = false;
    };
  
  #
  # collections of build tasks
  #

  basicAttrs = {
    inherit tarball;
    inherit build;
    inherit report;
    inherit dist;
    inherit profile;
  };

  # artificial dependency on report to ensure that we are not going through
  # an expensive regression test when there is already something wrong with
  # the build process.
  reportFirst = x : if report == null then x else x;
  testAttrs = reportFirst {
    inherit regress;
    inherit test;
  };

in basicAttrs // (if performRegress then testAttrs else {})
