#!/usr/bin/env ruby # This part of hte code is under construction. It will eventually be a script that # helps us check that the derivations we care about are all building, # and prints the status of those builds. # This requires Ruby 2.5.0 or later because it uses a new syntax for rescuing # exceptions in a block with needing to make an extra begin/end pair. require 'open3' require 'pathname' require 'set' require 'sqlite3' # gem install sqlite3 require_relative 'graph' require_relative 'expand_brackets' ResultsDir = Pathname('support/results') class AnticipatedError < RuntimeError end # Don't automatically change directory because maybe people want to test one # nixcrpkgs repository using the test script from another one. But do give an # early, friendly warning if they are running in the wrong directory. def check_directory! return if File.directory?('pretend_stdenv') $stderr.puts "You should run this script from the nixcrpkgs directory." dir = Pathname(__FILE__).parent.parent $stderr.puts "Try running these commands:\n cd #{dir}\n test/test.rb" exit 1 end def substitute_definitions(defs, str) str.gsub(/\$([\w-]+)/) do |x| defs.fetch($1) end end def parse_derivation_list(filename) defs = {} all_paths = Set.new all_attrs = {} File.foreach(filename).with_index do |line, line_index| line.strip! # Handle empty lines and comments. next if line.empty? || line.start_with?('#') # Handle variable definitions (e.g. "define windows = win32,win64"). if line.start_with?('define') md = line.match(/^define\s+([\w-]+)\s*=\s*(.*)$/) if !md raise AnticipatedError, "Invalid definition syntax." end name, value = md[1], md[2] defs[name] = value next end # Expand variable definitions (e.g. $windows expands to "win32,win64"). line = substitute_definitions(defs, line) # Figure out which parts of the line are attribute paths with brackets and # which are attributes. items = line.split(' ') attr_defs, path_items = items.partition { |p| p.include?('=') } # Expand any brackets in the attribute paths to get the complete list of # paths specified on this line. paths = path_items.flat_map { |p| expand_brackets(p) }.map(&:to_sym) # Process attribute definitions on the line, like "priority=1". attrs = {} attr_defs.each do |attr_def| md = attr_def.match(/^(\w+)=(\d+)$/) if !md raise AnticipatedError, "Invalid attribute definition: #{attr_def.inspect}." end name, value = md[1], md[2] case name when 'priority', 'slow' attrs[name.to_sym] = value.to_i else raise AnticipatedError, "Unrecognized attribute: #{name.inspect}." end end # Record the paths for this line and the attributes for those paths, # overriding previous attributes values if necessary. all_paths += paths if !attrs.empty? paths.each do |path| (all_attrs[path] ||= {}).merge!(attrs) end end rescue AnticipatedError => e raise AnticipatedError, "#{filename}:#{line_index + 1}: error: #{e}" end if all_paths.empty? raise AnticipatedError, "#{filename} specifies no paths" end all_paths.each do |path| if !path.match?(/^[\w.-]+$/) raise "Invalid characters in path name: #{path}" end end { defs: defs, paths: all_paths.to_a, attrs: all_attrs } end # Make a hash holding the priority of each Nix attribute path we want to build. # This routine determines the default priority. def make_path_priority_map(settings) attrs = settings.fetch(:attrs) m = {} settings.fetch(:paths).each do |path| m[path] = attrs.fetch(path, {}).fetch(:priority, 0) end m end # Make a hash holding the relative build time of each Nix attribute path we want # to build. This routine detrmines the default time, and what "slow" means. def make_path_time_map(settings) attrs = settings.fetch(:attrs) m = {} settings.fetch(:paths).each do |path| m[path] = attrs.fetch(path, {})[:slow] ? 100 : 1 end m end def instantiate_drvs(paths) cmd = 'nix-instantiate ' + paths.map { |p| "-A #{p}" }.join(' ') stdout_str, stderr_str, status = Open3.capture3(cmd) if !status.success? $stderr.puts stderr_str raise AnticipatedError, "Failed to instantiate derivations." end paths.zip(stdout_str.split.map(&:to_sym)).to_h end # We want there to be a one-to-one mapping between paths in the derivations.txt # list and derivations, so we can make a graph of dependencies of the # derivations and each derivation in the graph will have a unique path in the # derivations.txt list. def check_paths_are_unique!(path_drv_map) set = Set.new path_drv_map.each do |key, drv| if set.include?(drv) raise AnticipatedError, "The derivation #{key} is the same as " \ "other derivations in the list. Maybe use the 'omni' namespace." end set << drv end end # Makes a new map that has the same keys as map1, and the values # have all been mapped by map2. # # Requires map2 to have a key for every value in map1. def map_compose(map1, map2) map1.transform_values &map2.method(:fetch) end # Like map_compose, but excludes keys from map1 where the corresponding map1 # value is not a key of map2. def map_join(map1, map2) r = {} map1.each do |key, value| if map2.key?(value) r[key] = map2.fetch(value) end end r end def nix_db return $db if $db $db = SQLite3::Database.new '/nix/var/nix/db/db.sqlite', readonly: true end # Given an array of derivations (paths to .drv files in /nix), this function # queries the Nix database and returns hash table mapping derivations to # a boolean that is true if they have already been built. def get_build_status(drvs) drv_list_str = drvs.map { |d| "\"#{d}\"" }.join(", ") query = < 0 more_attrs << " penwidth=3" end # Draw slow nodes as a double octagon. if path_time_map.fetch(path) > 10 more_attrs << " shape=doubleoctagon" end f.puts "\"#{path}\" [label=\"#{component}\"#{more_attrs}]" end f.puts "}" end # Output dependencies between nodes. visible_paths.each do |path| path_graph.fetch(path).each do |dep| next if decompose.(dep).first == 'omni' f.puts "\"#{path}\" -> \"#{dep}\"" end end f.puts "}" end end def make_build_plan(path_state) path_graph = path_state.fetch(:graph) path_priority_map = path_state.fetch(:priority_map) path_time_map = path_state.fetch(:time_map) path_built_map = path_state.fetch(:built_map) # It's handy to be able to get all the dependencies of a node in one step, and # we will use that frequently to calculate how expensive it is to build a # node and to make the toplogical sort. path_graph = transitive_closure(path_graph).freeze # The paths we need to build. In the future we could filter this by priority. required_paths = Set.new(path_graph.keys).freeze # built_paths: The set of paths that are already built. We will mutate this # as we simulate our build plan. built_paths = Set.new path_built_map.each do |path, built| built_paths << path if built end # List of paths to build. Each path should only be built once all the paths it # depends on are built. I know nix-build can take care of that for us, but it's # nice to see the precise order of what is going to be built so we can tell when # slow things will get built. build_plan = [] # Computes the time to build a path, taking into account what has already been # built. calculate_time = lambda do |path| deps = path_graph.fetch(path) + [path] deps.reject! &built_paths.method(:include?) deps.map(&path_time_map.method(:fetch)).sum end # Adds plans to build this path and all of its unbuilt depedencies. add_to_build_plan = lambda do |path| deps = path_graph.fetch(path) + [path] # Remove dependencies that are already built. deps.reject! &built_paths.method(:include?) # Topological sort deps.sort! do |p1, p2| case when path_graph.fetch(p1).include?(p2) then 1 when path_graph.fetch(p2).include?(p1) then -1 else 0 end end deps.each do |path| build_plan << path built_paths << path end end while true unbuilt_required_paths = required_paths - built_paths break if unbuilt_required_paths.empty? # Find the maximum priority of the unbuilt required paths. max_priority = nil unbuilt_required_paths.each do |path| priority = path_priority_map.fetch(path) if !max_priority || priority > max_priority max_priority = priority end end top_priority_paths = unbuilt_required_paths.select do |path| path_priority_map.fetch(path) == max_priority end target = top_priority_paths.min_by(&calculate_time) add_to_build_plan.(target) end build_plan end # Updates the 'support/results' directory, which holds # symbolic links to all the derivations defined by nixcrpkgs and # listed in support/derivations.txt which have already been built. # # Intended use: # ln -s $PWD/support/results /nix/var/nix/gcroots/nixcrpkgs-results # support/manage results # nix-collect-garbage def update_results_dir(path_valid_results_map) ResultsDir.mkdir if !ResultsDir.directory? ResultsDir.children.each do |p| p.unlink end modern_links = Set.new path_valid_results_map.each do |path, results_map| results_map.each do |id, result| suffix = id == :out ? '' : ".#{id}" link_name = "#{path}#{suffix}" (ResultsDir + link_name).make_symlink(result) modern_links << link_name end end end def build_paths(path_graph, path_built_map, build_plan, keep_going: true) path_built_map = path_built_map.dup path_graph = transitive_closure(path_graph) build_plan.each do |path| if !path_graph.fetch(path).all?(&path_built_map.method(:fetch)) # One of the dependencies of this path has not been built, presumably # because there was an error. puts "# skipping #{path}" next end print "nix-build -A #{path}" system("nix-build -A #{path} > /dev/null 2> /dev/null") if $?.success? path_built_map[path] = true puts else puts " # failed" return false if !keep_going end end true end def parse_args(argv) action = case argv.first when 'graph' then :graph when 'results' then :results when 'build' then :build when 'plan' then :plan when 'stats', nil then :stats else raise AnticipatedError, "Invalid action: #{argv.first.inspect}" end { action: action } end begin check_directory! args = parse_args(ARGV) action = args.fetch(:action) settings = parse_derivation_list('support/derivations.txt') path_drv_map = instantiate_drvs(settings.fetch(:paths)) check_paths_are_unique!(path_drv_map) drvs = path_drv_map.values.uniq drv_built_map = get_build_status(drvs) if [:graph, :build, :plan].include?(action) global_drv_graph = get_drv_graph drv_graph = graph_restrict_nodes(global_drv_graph, drvs) path_state = { graph: graph_unmap(drv_graph, path_drv_map).freeze, priority_map: make_path_priority_map(settings).freeze, time_map: make_path_time_map(settings).freeze, built_map: map_compose(path_drv_map, drv_built_map).freeze, }.freeze end if action == :graph output_graphviz(path_state) end if [:build, :plan].include?(action) build_plan = make_build_plan(path_state) end if action == :plan puts "Build plan:" build_plan.each do |path| puts "nix-build -A #{path}" end end if action == :build success = build_paths(path_state[:graph], path_state[:built_map], build_plan) exit(1) if !success end if action == :results || action == :build drv_valid_results_map = get_valid_results(drvs) path_valid_results_map = map_join(path_drv_map, drv_valid_results_map).freeze update_results_dir(path_valid_results_map) end if action == :stats print_stats(drv_built_map) end rescue AnticipatedError => e $stderr.puts e end