#!/usr/bin/perl -w # SPDX-License-Identifier: GPL-2.0-only # # Copyright 2015 - Steven Rostedt, Red Hat Inc. # Copyright 2017 - Steven Rostedt, VMware, Inc. # # usage: # config-bisect.pl [options] good-config bad-config [good|bad] # # Compares a good config to a bad config, then takes half of the diffs # and produces a config that is somewhere between the good config and # the bad config. That is, the resulting config will start with the # good config and will try to make half of the differences of between # the good and bad configs match the bad config. It tries because of # dependencies between the two configs it may not be able to change # exactly half of the configs that are different between the two config # files. # Here's a normal way to use it: # # $ cd /path/to/linux/kernel # $ config-bisect.pl /path/to/good/config /path/to/bad/config # This will now pull in good config (blowing away .config in that directory # so do not make that be one of the good or bad configs), and then # build the config with "make oldconfig" to make sure it matches the # current kernel. It will then store the configs in that result for # the good config. It does the same for the bad config as well. # The algorithm will run, merging half of the differences between # the two configs and building them with "make oldconfig" to make sure # the result changes (dependencies may reset changes the tool had made). # It then copies the result of its good config to /path/to/good/config.tmp # and the bad config to /path/to/bad/config.tmp (just appends ".tmp" to the # files passed in). And the ".config" that you should test will be in # directory # After the first run, determine if the result is good or bad then # run the same command appending the result # For good results: # $ config-bisect.pl /path/to/good/config /path/to/bad/config good # For bad results: # $ config-bisect.pl /path/to/good/config /path/to/bad/config bad # Do not change the good-config or bad-config, config-bisect.pl will # copy the good-config to a temp file with the same name as good-config # but with a ".tmp" after it. It will do the same with the bad-config. # If "good" or "bad" is not stated at the end, it will copy the good and # bad configs to the .tmp versions. If a .tmp version already exists, it will # warn before writing over them (-r will not warn, and just write over them). # If the last config is labeled "good", then it will copy it to the good .tmp # version. If the last config is labeled "bad", it will copy it to the bad # .tmp version. It will continue this until it can not merge the two any more # without the result being equal to either the good or bad .tmp configs. my $start = 0; my $val = ""; my $pwd = `pwd`; chomp $pwd; my $tree = $pwd; my $build; my $output_config; my $reset_bisect; sub usage { print << "EOF" usage: config-bisect.pl [-l linux-tree][-b build-dir] good-config bad-config [good|bad] -l [optional] define location of linux-tree (default is current directory) -b [optional] define location to build (O=build-dir) (default is linux-tree) good-config the config that is considered good bad-config the config that does not work "good" add this if the last run produced a good config "bad" add this if the last run produced a bad config If "good" or "bad" is not specified, then it is the start of a new bisect Note, each run will create copy of good and bad configs with ".tmp" appended. EOF ; exit(-1); } sub doprint { print @_; } sub dodie { doprint "CRITICAL FAILURE... ", @_, "\n"; die @_, "\n"; } sub expand_path { my ($file) = @_; if ($file =~ m,^/,) { return $file; } return "$pwd/$file"; } sub read_prompt { my ($cancel, $prompt) = @_; my $ans; for (;;) { if ($cancel) { print "$prompt [y/n/C] "; } else { print "$prompt [y/N] "; } $ans = <STDIN>; chomp $ans; if ($ans =~ /^\s*$/) { if ($cancel) { $ans = "c"; } else { $ans = "n"; } } last if ($ans =~ /^y$/i || $ans =~ /^n$/i); if ($cancel) { last if ($ans =~ /^c$/i); print "Please answer either 'y', 'n' or 'c'.\n"; } else { print "Please answer either 'y' or 'n'.\n"; } } if ($ans =~ /^c/i) { exit; } if ($ans !~ /^y$/i) { return 0; } return 1; } sub read_yn { my ($prompt) = @_; return read_prompt 0, $prompt; } sub read_ync { my ($prompt) = @_; return read_prompt 1, $prompt; } sub run_command { my ($command, $redirect) = @_; my $start_time; my $end_time; my $dord = 0; my $pid; $start_time = time; doprint("$command ... "); $pid = open(CMD, "$command 2>&1 |") or dodie "unable to exec $command"; if (defined($redirect)) { open (RD, ">$redirect") or dodie "failed to write to redirect $redirect"; $dord = 1; } while (<CMD>) { print RD if ($dord); } waitpid($pid, 0); my $failed = $?; close(CMD); close(RD) if ($dord); $end_time = time; my $delta = $end_time - $start_time; if ($delta == 1) { doprint "[1 second] "; } else { doprint "[$delta seconds] "; } if ($failed) { doprint "FAILED!\n"; } else { doprint "SUCCESS\n"; } return !$failed; } ###### CONFIG BISECT ###### # config_ignore holds the configs that were set (or unset) for # a good config and we will ignore these configs for the rest # of a config bisect. These configs stay as they were. my %config_ignore; # config_set holds what all configs were set as. my %config_set; # config_off holds the set of configs that the bad config had disabled. # We need to record them and set them in the .config when running # olddefconfig, because olddefconfig keeps the defaults. my %config_off; # config_off_tmp holds a set of configs to turn off for now my @config_off_tmp; # config_list is the set of configs that are being tested my %config_list; my %null_config; my %dependency; my $make; sub make_oldconfig { if (!run_command "$make olddefconfig") { # Perhaps olddefconfig doesn't exist in this version of the kernel # try oldnoconfig doprint "olddefconfig failed, trying make oldnoconfig\n"; if (!run_command "$make oldnoconfig") { doprint "oldnoconfig failed, trying yes '' | make oldconfig\n"; # try a yes '' | oldconfig run_command "yes '' | $make oldconfig" or dodie "failed make config oldconfig"; } } } sub assign_configs { my ($hash, $config) = @_; doprint "Reading configs from $config\n"; open (IN, $config) or dodie "Failed to read $config"; while (<IN>) { chomp; if (/^((CONFIG\S*)=.*)/) { ${$hash}{$2} = $1; } elsif (/^(# (CONFIG\S*) is not set)/) { ${$hash}{$2} = $1; } } close(IN); } sub process_config_ignore { my ($config) = @_; assign_configs \%config_ignore, $config; } sub get_dependencies { my ($config) = @_; my $arr = $dependency{$config}; if (!defined($arr)) { return (); } my @deps = @{$arr}; foreach my $dep (@{$arr}) { print "ADD DEP $dep\n"; @deps = (@deps, get_dependencies $dep); } return @deps; } sub save_config { my ($pc, $file) = @_; my %configs = %{$pc}; doprint "Saving configs into $file\n"; open(OUT, ">$file") or dodie "Can not write to $file"; foreach my $config (keys %configs) { print OUT "$configs{$config}\n"; } close(OUT); } sub create_config { my ($name, $pc) = @_; doprint "Creating old config from $name configs\n"; save_config $pc, $output_config; make_oldconfig; } # compare two config hashes, and return configs with different vals. # It returns B's config values, but you can use A to see what A was. sub diff_config_vals { my ($pa, $pb) = @_; # crappy Perl way to pass in hashes. my %a = %{$pa}; my %b = %{$pb}; my %ret; foreach my $item (keys %a) { if (defined($b{$item}) && $b{$item} ne $a{$item}) { $ret{$item} = $b{$item}; } } return %ret; } # compare two config hashes and return the configs in B but not A sub diff_configs { my ($pa, $pb) = @_; my %ret; # crappy Perl way to pass in hashes. my %a = %{$pa}; my %b = %{$pb}; foreach my $item (keys %b) { if (!defined($a{$item})) { $ret{$item} = $b{$item}; } } return %ret; } # return if two configs are equal or not # 0 is equal +1 b has something a does not # +1 if a and b have a different item. # -1 if a has something b does not sub compare_configs { my ($pa, $pb) = @_; my %ret; # crappy Perl way to pass in hashes. my %a = %{$pa}; my %b = %{$pb}; foreach my $item (keys %b) { if (!defined($a{$item})) { return 1; } if ($a{$item} ne $b{$item}) { return 1; } } foreach my $item (keys %a) { if (!defined($b{$item})) { return -1; } } return 0; } sub process_failed { my ($config) = @_; doprint "\n\n***************************************\n"; doprint "Found bad config: $config\n"; doprint "***************************************\n\n"; } sub process_new_config { my ($tc, $nc, $gc, $bc) = @_; my %tmp_config = %{$tc}; my %good_configs = %{$gc}; my %bad_configs = %{$bc}; my %new_configs; my $runtest = 1; my $ret; create_config "tmp_configs", \%tmp_config; assign_configs \%new_configs, $output_config; $ret = compare_configs \%new_configs, \%bad_configs; if (!$ret) { doprint "New config equals bad config, try next test\n"; $runtest = 0; } if ($runtest) { $ret = compare_configs \%new_configs, \%good_configs; if (!$ret) { doprint "New config equals good config, try next test\n"; $runtest = 0; } } %{$nc} = %new_configs; return $runtest; } sub convert_config { my ($config) = @_; if ($config =~ /^# (.*) is not set/) { $config = "$1=n"; } $config =~ s/^CONFIG_//; return $config; } sub print_config { my ($sym, $config) = @_; $config = convert_config $config; doprint "$sym$config\n"; } sub print_config_compare { my ($good_config, $bad_config) = @_; $good_config = convert_config $good_config; $bad_config = convert_config $bad_config; my $good_value = $good_config; my $bad_value = $bad_config; $good_value =~ s/(.*)=//; my $config = $1; $bad_value =~ s/.*=//; doprint " $config $good_value -> $bad_value\n"; } # Pass in: # $phalf: half of the configs names you want to add # $oconfigs: The orginial configs to start with # $sconfigs: The source to update $oconfigs with (from $phalf) # $which: The name of which half that is updating (top / bottom) # $type: The name of the source type (good / bad) sub make_half { my ($phalf, $oconfigs, $sconfigs, $which, $type) = @_; my @half = @{$phalf}; my %orig_configs = %{$oconfigs}; my %source_configs = %{$sconfigs}; my %tmp_config = %orig_configs; doprint "Settings bisect with $which half of $type configs:\n"; foreach my $item (@half) { doprint "Updating $item to $source_configs{$item}\n"; $tmp_config{$item} = $source_configs{$item}; } return %tmp_config; } sub run_config_bisect { my ($pgood, $pbad) = @_; my %good_configs = %{$pgood}; my %bad_configs = %{$pbad}; my %diff_configs = diff_config_vals \%good_configs, \%bad_configs; my %b_configs = diff_configs \%good_configs, \%bad_configs; my %g_configs = diff_configs \%bad_configs, \%good_configs; # diff_arr is what is in both good and bad but are different (y->n) my @diff_arr = keys %diff_configs; my $len_diff = $#diff_arr + 1; # b_arr is what is in bad but not in good (has depends) my @b_arr = keys %b_configs; my $len_b = $#b_arr + 1; # g_arr is what is in good but not in bad my @g_arr = keys %g_configs; my $len_g = $#g_arr + 1; my $runtest = 0; my %new_configs; my $ret; # Look at the configs that are different between good and bad. # This does not include those that depend on other configs # (configs depending on other configs that are not set would # not show up even as a "# CONFIG_FOO is not set" doprint "# of configs to check: $len_diff\n"; doprint "# of configs showing only in good: $len_g\n"; doprint "# of configs showing only in bad: $len_b\n"; if ($len_diff > 0) { # Now test for different values doprint "Configs left to check:\n"; doprint " Good Config\t\t\tBad Config\n"; doprint " -----------\t\t\t----------\n"; foreach my $item (@diff_arr) { doprint " $good_configs{$item}\t$bad_configs{$item}\n"; } my $half = int($#diff_arr / 2); my @tophalf = @diff_arr[0 .. $half]; doprint "Set tmp config to be good config with some bad config values\n"; my %tmp_config = make_half \@tophalf, \%good_configs, \%bad_configs, "top", "bad"; $runtest = process_new_config \%tmp_config, \%new_configs, \%good_configs, \%bad_configs; if (!$runtest) { doprint "Set tmp config to be bad config with some good config values\n"; my %tmp_config = make_half \@tophalf, \%bad_configs, \%good_configs, "top", "good"; $runtest = process_new_config \%tmp_config, \%new_configs, \%good_configs, \%bad_configs; } } if (!$runtest && $len_diff > 0) { # do the same thing, but this time with bottom half my $half = int($#diff_arr / 2); my @bottomhalf = @diff_arr[$half+1 .. $#diff_arr]; doprint "Set tmp config to be good config with some bad config values\n"; my %tmp_config = make_half \@bottomhalf, \%good_configs, \%bad_configs, "bottom", "bad"; $runtest = process_new_config \%tmp_config, \%new_configs, \%good_configs, \%bad_configs; if (!$runtest) { doprint "Set tmp config to be bad config with some good config values\n"; my %tmp_config = make_half \@bottomhalf, \%bad_configs, \%good_configs, "bottom", "good"; $runtest = process_new_config \%tmp_config, \%new_configs, \%good_configs, \%bad_configs; } } if ($runtest) { make_oldconfig; doprint "READY TO TEST .config IN $build\n"; return 0; } doprint "\n%%%%%%%% FAILED TO FIND SINGLE BAD CONFIG %%%%%%%%\n"; doprint "Hmm, can't make any more changes without making good == bad?\n"; doprint "Difference between good (+) and bad (-)\n"; foreach my $item (keys %bad_configs) { if (!defined($good_configs{$item})) { print_config "-", $bad_configs{$item}; } } foreach my $item (keys %good_configs) { next if (!defined($bad_configs{$item})); if ($good_configs{$item} ne $bad_configs{$item}) { print_config_compare $good_configs{$item}, $bad_configs{$item}; } } foreach my $item (keys %good_configs) { if (!defined($bad_configs{$item})) { print_config "+", $good_configs{$item}; } } return -1; } sub config_bisect { my ($good_config, $bad_config) = @_; my $ret; my %good_configs; my %bad_configs; my %tmp_configs; doprint "Run good configs through make oldconfig\n"; assign_configs \%tmp_configs, $good_config; create_config "$good_config", \%tmp_configs; assign_configs \%good_configs, $output_config; doprint "Run bad configs through make oldconfig\n"; assign_configs \%tmp_configs, $bad_config; create_config "$bad_config", \%tmp_configs; assign_configs \%bad_configs, $output_config; save_config \%good_configs, $good_config; save_config \%bad_configs, $bad_config; return run_config_bisect \%good_configs, \%bad_configs; } while ($#ARGV >= 0) { if ($ARGV[0] !~ m/^-/) { last; } my $opt = shift @ARGV; if ($opt eq "-b") { $val = shift @ARGV; if (!defined($val)) { die "-b requires value\n"; } $build = $val; } elsif ($opt eq "-l") { $val = shift @ARGV; if (!defined($val)) { die "-l requires value\n"; } $tree = $val; } elsif ($opt eq "-r") { $reset_bisect = 1; } elsif ($opt eq "-h") { usage; } else { die "Unknown option $opt\n"; } } $build = $tree if (!defined($build)); $tree = expand_path $tree; $build = expand_path $build; if ( ! -d $tree ) { die "$tree not a directory\n"; } if ( ! -d $build ) { die "$build not a directory\n"; } usage if $#ARGV < 1; if ($#ARGV == 1) { $start = 1; } elsif ($#ARGV == 2) { $val = $ARGV[2]; if ($val ne "good" && $val ne "bad") { die "Unknown command '$val', bust be either \"good\" or \"bad\"\n"; } } else { usage; } my $good_start = expand_path $ARGV[0]; my $bad_start = expand_path $ARGV[1]; my $good = "$good_start.tmp"; my $bad = "$bad_start.tmp"; $make = "make"; if ($build ne $tree) { $make = "make O=$build" } $output_config = "$build/.config"; if ($start) { if ( ! -f $good_start ) { die "$good_start not found\n"; } if ( ! -f $bad_start ) { die "$bad_start not found\n"; } if ( -f $good || -f $bad ) { my $p = ""; if ( -f $good ) { $p = "$good exists\n"; } if ( -f $bad ) { $p = "$p$bad exists\n"; } if (!defined($reset_bisect)) { if (!read_yn "${p}Overwrite and start new bisect anyway?") { exit (-1); } } } run_command "cp $good_start $good" or die "failed to copy to $good\n"; run_command "cp $bad_start $bad" or die "failed to copy to $bad\n"; } else { if ( ! -f $good ) { die "Can not find file $good\n"; } if ( ! -f $bad ) { die "Can not find file $bad\n"; } if ($val eq "good") { run_command "cp $output_config $good" or die "failed to copy $config to $good\n"; } elsif ($val eq "bad") { run_command "cp $output_config $bad" or die "failed to copy $config to $bad\n"; } } chdir $tree || die "can't change directory to $tree"; my $ret = config_bisect $good, $bad; if (!$ret) { exit(0); } if ($ret > 0) { doprint "Cleaning temp files\n"; run_command "rm $good"; run_command "rm $bad"; exit(1); } else { doprint "See good and bad configs for details:\n"; doprint "good: $good\n"; doprint "bad: $bad\n"; doprint "%%%%%%%% FAILED TO FIND SINGLE BAD CONFIG %%%%%%%%\n"; } exit(2);