diff --git a/modules/ksb/BuildSystem.pm b/modules/ksb/BuildSystem.pm index 0562edb..d17d5fc 100644 --- a/modules/ksb/BuildSystem.pm +++ b/modules/ksb/BuildSystem.pm @@ -1,448 +1,441 @@ package ksb::BuildSystem 0.30; # Base module for the various build systems, includes built-in implementations of # generic functions and supports hooks for subclasses to provide needed detailed # functionality. use strict; use warnings; use 5.014; use ksb::BuildException; use ksb::Debug 0.30; use ksb::Util; use ksb::StatusView; use List::Util qw(first); sub new { my ($class, $module) = @_; my $self = bless { module => $module }, $class; # This is simply the 'default' build system at this point, also used for # KF5. if ($class ne 'ksb::BuildSystem::KDE4') { _maskGlobalBuildSystemOptions($self); } return $self; } # Removes or masks global build system-related options, so that they aren't # accidentally picked up for use with our non-default build system. # Module-specific options are left intact. sub _maskGlobalBuildSystemOptions { my $self = shift; my $module = $self->module(); my $ctx = $module->buildContext(); my @buildSystemOptions = qw( cmake-options cmake-generator configure-flags custom-build-command cxxflags make-options run-tests use-clean-install ); for my $opt (@buildSystemOptions) { # If an option is present, and not set at module-level, it must be # global. Can't use getOption() method due to recursion. if ($ctx->{options}->{$opt} && !$module->{options}->{$opt}) { $module->{options}->{$opt} = ''; } } } sub module { my $self = shift; return $self->{module}; } # Subroutine to determine if a given module needs to have the build system # recreated from scratch. # If so, it returns a non empty string sub needsRefreshed { my $self = assert_isa(shift, 'ksb::BuildSystem'); my $module = $self->module(); my $builddir = $module->fullpath('build'); my $confFileKey = $self->configuredModuleFileName(); if (not -e "$builddir") { return "the build directory doesn't exist"; } if (-e "$builddir/.refresh-me") { return "the last configure failed"; # see Module.pm } if ($module->getOption("refresh-build")) { return "the option refresh-build was set"; } if (not -e "$builddir/$confFileKey") { return "$builddir/$confFileKey is missing"; } return ""; } # Called by the module being built before it runs its build/install process. Should # setup any needed environment variables, build context settings, etc., in preparation # for the build and install phases. sub prepareModuleBuildEnvironment { my ($self, $ctx, $module, $prefix) = @_; } # Returns true if the module should have make install run in order to be # used, or false if installation is not required or possible. sub needsInstalled { return 1; } # This should return a list of executable names that must be present to # even bother attempting to use this build system. An empty list should be # returned if there's no required programs. sub requiredPrograms { return; } sub name { return 'generic'; } # Returns a list of possible build commands to run, any one of which should # be supported by the build system. sub buildCommands { # Non Linux systems can sometimes fail to build when GNU Make would work, # so prefer GNU Make if present, otherwise try regular make. return 'gmake', 'make'; } sub defaultBuildCommand { my $self = shift; # Convert the path to an absolute path since I've encountered a sudo # that is apparently unable to guess. Maybe it's better that it # doesn't guess anyways from a security point-of-view. my $buildCommand = first { absPathToExecutable($_) } $self->buildCommands(); return $buildCommand; } # Return value style: hashref {was_successful => bool, warnings => int, ...} sub buildInternal { my $self = shift; my $optionsName = shift // 'make-options'; return $self->safe_make({ target => undef, message => 'Compiling...', 'make-options' => [ split(' ', $self->module()->getOption($optionsName)), ], logbase => 'build', }); } # Return value style: boolean sub configureInternal { # It is possible to make it here if there's no source dir and if we're # pretending. If we're not actually pretending then this should be a # bug... return 1 if pretending(); croak_internal('We were not supposed to get to this point...'); } # Returns name of file that should exist (relative to the module's build directory) # if the module has been configured. sub configuredModuleFileName { my $self = shift; return 'Makefile'; } # Runs the testsuite for the given module. # Returns true if a testsuite is present and all tests passed, false otherwise. sub runTestsuite { my $self = shift; my $module = $self->module(); whisper ("\ty[$module] does not support the b[run-tests] option"); return 0; } # Used to install a module (that has already been built, tested, etc.) # All options passed are prefixed to the eventual command to be run. # Returns boolean false if unable to install, true otherwise. sub installInternal { my $self = shift; my $module = $self->module(); my @cmdPrefix = @_; return $self->safe_make ({ target => 'install', message => 'Installing..', 'prefix-options' => [@cmdPrefix], })->{was_successful}; } # Used to uninstall a previously installed module. # All options passed are prefixed to the eventual command to be run. # Returns boolean false if unable to uninstall, true otherwise. sub uninstallInternal { my $self = shift; my $module = $self->module(); my @cmdPrefix = @_; return $self->safe_make ({ target => 'uninstall', message => "Uninstalling g[$module]", 'prefix-options' => [@cmdPrefix], })->{was_successful}; } # Subroutine to clean the build system for the given module. Works by # recursively deleting the directory and then recreating it. # Returns 0 for failure, non-zero for success. sub cleanBuildSystem { my $self = assert_isa(shift, 'ksb::BuildSystem'); my $module = $self->module(); my $srcdir = $module->fullpath('source'); my $builddir = $module->fullpath('build'); if (pretending()) { pretend ("\tWould have cleaned build system for g[$module]"); return 1; } # Use an existing directory if (-e $builddir && $builddir ne $srcdir) { whisper ("\tCleaning out build directory"); # This variant of log_command runs the sub prune_under_directory($builddir) # in a forked child, so that we can log its output. if (log_command($module, 'clean-builddir', [ 'kdesrc-build', 'main::prune_under_directory', $builddir ])) { error (" r[b[*]\tFailed to clean build directory. Verify the permissions are correct."); return 0; # False for this function. } # Let users know we're done so they don't wonder why rm -rf is taking so # long and oh yeah, why's my HD so active?... whisper ("\tOld build system cleaned, starting new build system."); } # or create the directory elsif (!super_mkdir ($builddir)) { error ("\tUnable to create directory r[$builddir]."); return 0; } return 1; } sub needsBuilddirHack { return 0; # By default all build systems are assumed to be sane } # Return convention: boolean sub createBuildSystem { my $self = assert_isa(shift, 'ksb::BuildSystem'); my $module = $self->module(); my $builddir = $module->fullpath('build'); my $srcdir = $module->fullpath('source'); if (! -e "$builddir" && !super_mkdir("$builddir")) { error ("\tUnable to create build directory for r[$module]!!"); return 0; } if ($builddir ne $srcdir && $self->needsBuilddirHack() && 0 != log_command($module, 'lndir', ['kdesrc-build', 'main::safe_lndir', $srcdir, $builddir])) { error ("\tUnable to setup symlinked build directory for r[$module]!!"); return 0; } return 1; } # Subroutine to run the build command with the arguments given by the # passed hash. # # Due to the various ways the build command is called by this script, it is # required to pass customization options in a hash: # { # target => undef, or a valid build target e.g. 'install', # message => 'Compiling.../Installing.../etc.' # make-options => [ list of command line arguments to pass to make. See # make-options ], # prefix-options => [ list of command line arguments to prefix *before* the # make command, used for make-install-prefix support for # e.g. sudo ], # logbase => 'base-log-filename', # } # # target and message are required. logbase is required if target is left # undefined, but otherwise defaults to the same value as target. # # Note that the make command is based on the results of the 'buildCommands' # subroutine which should be overridden if necessary by subclasses. Each # command should be the command name (i.e. no path). The user may override # the command used (for build only) by using the 'custom-build-command' # option. # # The first command name found which resolves to an executable on the # system will be used, if no command this function will fail. # # The first argument should be the ksb::Module object to be made. # The second argument should be the reference to the hash described above. # # Returns a hashref: # { # was_successful => $bool, (if successful) # } sub safe_make (@) { my ($self, $optsRef) = @_; assert_isa($self, 'ksb::BuildSystem'); my $module = $self->module(); my $buildCommand = $self->defaultBuildCommand(); my @buildCommandLine = $buildCommand; # Check for custom user command. We support command line options being # passed to the command as well. my $userCommand = $module->getOption('custom-build-command'); if ($userCommand) { @buildCommandLine = split_quoted_on_whitespace($userCommand); $buildCommand = absPathToExecutable($buildCommandLine[0]); } if (!$buildCommand) { $buildCommand = $userCommand || $self->buildCommands(); error (" r[b[*] Unable to find the g[$buildCommand] executable!"); return { was_successful => 0 }; } # Make it prettier if pretending (Remove leading directories). $buildCommand =~ s{^/.*/}{} if pretending(); shift @buildCommandLine; # $buildCommand is already the first entry. # Simplify code by forcing lists to exist. $optsRef->{'prefix-options'} //= [ ]; $optsRef->{'make-options'} //= [ ]; my @prefixOpts = @{$optsRef->{'prefix-options'}}; # If using sudo ensure that it doesn't wait on tty, but tries to read from # stdin (which should fail as we redirect that from /dev/null) if (@prefixOpts && $prefixOpts[0] eq 'sudo' && !grep { /^-S$/ } @prefixOpts) { splice (@prefixOpts, 1, 0, '-S'); # Add -S right after 'sudo' } # Assemble arguments my @args = (@prefixOpts, $buildCommand, @buildCommandLine); push @args, $optsRef->{target} if $optsRef->{target}; push @args, @{$optsRef->{'make-options'}}; # Will be output by _runBuildCommand my $buildMessage = $optsRef->{message}; my $logname = $optsRef->{logbase} // $optsRef->{logfile} // $optsRef->{target}; p_chdir ($module->fullpath('build')); return $self->_runBuildCommand($buildMessage, $logname, \@args); } # Subroutine to run make and process the build process output in order to # provide completion updates. This procedure takes the same arguments as # log_command() (described here as well), except that the callback argument # is not used. # # First parameter is the message to display to the user while the build # happens. # Second parameter is the name of the log file to use (relative to the log # directory). # Third parameter is a reference to an array with the command and its # arguments. i.e. ['command', 'arg1', 'arg2'] # # The return value is a hashref as defined by safe_make sub _runBuildCommand { my ($self, $message, $filename, $argRef) = @_; my $module = $self->module(); my $resultRef = { was_successful => 0 }; my $ctx = $module->buildContext(); # There are situations when we don't want progress output: # 1. If we're not printing to a terminal. # 2. When we're debugging (we'd interfere with debugging output). if (! -t STDERR || debugging()) { $resultRef->{was_successful} = (0 == log_command($module, $filename, $argRef)); return $resultRef; } # TODO More details my $warnings = 0; my $log_command_callback = sub { state $oldX = -1; state $oldY = -1; my $input = shift; return if not defined $input; $warnings++ if ($input =~ /warning: /); my ($x, $y); my ($percentage) = ($input =~ /^\[\s*([0-9]+)%]/); if ($percentage) { $x = int $percentage; $y = 100; } else { # ninja-syntax my ($newX, $newY) = ($input =~ /^\[([0-9]+)\/([0-9]+)] /); return unless ($newX && $newY); ($x, $y) = (int $newX, int $newY); } if ($x != $oldX || $y != $oldY) { ksb::Debug::reportProgressToParent($module, $x, $y); } }; $resultRef->{was_successful} = (0 == log_command($module, $filename, $argRef, { callback => $log_command_callback })); - $resultRef->{warnings} = $warnings; - if ($warnings) { - my $count = ($warnings < 3 ) ? 1 : - ($warnings < 10 ) ? 2 : - ($warnings < 30 ) ? 3 : 4; - my $msg = sprintf("%s b[y[$warnings] %s", '-' x $count, '-' x $count); - note ("\tNote: $msg compile warnings"); - $self->{module}->setPersistentOption('last-compile-warnings', $warnings); - } + $resultRef->{warnings} = $warnings; + $self->{module}->setPersistentOption('last-compile-warnings', $warnings); return $resultRef; } 1; diff --git a/modules/ksb/StatusView.pm b/modules/ksb/StatusView.pm index a0c6355..85e0a49 100644 --- a/modules/ksb/StatusView.pm +++ b/modules/ksb/StatusView.pm @@ -1,440 +1,449 @@ package ksb::StatusView 0.30; use utf8; # Source code is utf8-encoded # Helper used to handle a generic 'progress update' status for the module # build, update, install, etc. processes. # # Currently supports TTY output only but it's not impossible to visualize # extending this to a GUI or even web server as options. use strict; use warnings; use 5.014; # our output to STDOUT should match locale (esp UTF-8 locale, which induces # warnings for UTF-8 output unless we specifically opt-in) use open OUT => ':locale'; use ksb::Debug 0.20 qw(colorize); use ksb::Util; use ksb::BuildException; use List::Util qw(min max reduce first); use IO::Handle; sub new { my $class = shift; my $tty_width = int(`tput cols` // $ENV{COLUMNS} // 80); my $defaultOpts = { tty_width => $tty_width, max_name_width => 1, # Updated from the build plan cur_update => '', # moduleName under update cur_working => '', # moduleName under any other phase cur_progress => '', # Percentage (0% - 100%) module_in_phase => { }, # $phase -> $moduleName done_in_phase => { }, # $phase -> int todo_in_phase => { }, # $phase -> int failed_at_phase => { }, # $moduleName -> $phase log_entries => { }, # $moduleName -> $phase -> [ $entry ... ] last_mod_entry => '', # $moduleName/$phase, see onLogEntries last_msg_type => '', # If 'progress' we can clear line + warnings => { }, # $moduleName -> $sum_of_warnings }; # Must bless a hash ref since subclasses expect it. return bless $defaultOpts, $class; } # Accepts a single event, as a hashref decoded from its source JSON format (as # described in ksb::StatusMonitor), and updates the user interface # appropriately. sub notifyEvent { my ($self, $ev) = @_; state $handlers = { phase_started => \&onPhaseStarted, phase_progress => \&onPhaseProgress, phase_completed => \&onPhaseCompleted, build_plan => \&onBuildPlan, build_done => \&onBuildDone, log_entries => \&onLogEntries, }; state $err = sub { croak_internal("Invalid event! $_[1]"); }; my $handler = $handlers->{$ev->{event}} // $err; # This is a method call though we don't use normal Perl method call syntax $handler->($self, $ev); } # Event handlers # A module has started on a given phase. Multiple phases can be in-flight at # once! sub onPhaseStarted { my ($self, $ev) = @_; my ($moduleName, $phase) = @{$ev->{phase_started}}{qw/module phase/}; $self->{module_in_phase}->{$phase} = $moduleName; my $phaseKey = $phase eq 'update' ? 'cur_update' : 'cur_working'; $self->{$phaseKey} = $moduleName; $self->update(); } # Progress has been made within a phase of a module build. Only supported for # the build phase, currently. sub onPhaseProgress { my ($self, $ev) = @_; my ($moduleName, $phase, $progress) = @{$ev->{phase_progress}}{qw/module phase progress/}; $progress = sprintf ("%3.1f", 100.0 * $progress); $self->{cur_progress} = $progress; $self->update(); } # Writes out a line to TTY noting information about the module that just finished # (elapsed time, compile warnings, success/failure, etc.) # Pass in the monitor event for the 'phase_completed' event sub _showModuleFinishResults { my ($self, $ev) = @_; my ($moduleName, $phase, $result) = @{$ev->{phase_completed}}{qw/module phase result/}; my $modulePhasePlan = $self->{planned_phases}->{$moduleName}; my %shortPhases = ( update => 'Upd', buildsystem => 'Cnf', build => 'Bld', test => 'Tst', install => 'Ins', uninstall => 'Uns', ); my %resultColors = ( 'success' => 'g', 'error' => 'r', 'skipped' => 'y', 'pending' => 'y', ); # Locate this module's specific build plan from the ordered array my $modulePlan = first { $_->{name} eq $moduleName } @{$self->{build_plan}}; # Turn each planned phase into a colorized representation of its success or failure my $done_phases = join(' / ', map { my $clr = $resultColors{$modulePhasePlan->{$_}} // 'y'; "$clr" . "[$shortPhases{$_}]" } @{$modulePlan->{phases}}); my $overallColor = $resultColors{$result} // ''; # Space out module names so that the whole list is table-aligned my $fixedLengthName = sprintf("%-*s", $self->{max_name_width}, $moduleName); my $printedTime = prettify_seconds($ev->{phase_completed}->{elapsed} // 0); - $self->_clearLineAndUpdate(colorize(" ${overallColor}[b[*] Completed b[$fixedLengthName] $printedTime $done_phases\n")); + my $notes = ''; + my $warnings = $self->{warnings}->{$moduleName}; + $notes .= "$warnings compiler warnings" if $warnings; + + my $msg = " ${overallColor}[b[*] Completed b[$fixedLengthName] $printedTime $done_phases | $notes"; + $self->_clearLineAndUpdate(colorize("$msg\n")); } # A phase of a module build is finished sub onPhaseCompleted { my ($self, $ev) = @_; my ($moduleName, $phase, $result) = @{$ev->{phase_completed}}{qw/module phase result/}; my $modulePhasePlan = $self->{planned_phases}->{$moduleName}; $self->_checkForBuildPlan(); $modulePhasePlan->{$phase} = $result; + $self->{warnings}->{$moduleName} //= 0; + $self->{warnings}->{$moduleName} += $ev->{phase_completed}->{warnings} // 0; + if ($result eq 'error') { $self->{failed_at_phase}->{$moduleName} = $phase; # The phases should all eventually become failed but we should # still flag them here in case they don't while (my ($phase, $result) = each %{$modulePhasePlan}) { $modulePhasePlan->{$phase} = 'skipped' if $result eq 'pending'; } } # Are we completely done building the module? if (!first { $_ eq 'pending' } values %{$modulePhasePlan}) { $self->_showModuleFinishResults($ev); } # Update global progress bar $self->{done_in_phase}->{$phase}++; my $phase_done = ( ($self->{done_in_phase}->{$phase} // 0) == ($self->{todo_in_phase}->{$phase} // 999)); my $phaseKey = $phase eq 'update' ? 'cur_update' : 'cur_working'; $self->{$phaseKey} = $phase_done ? '---' : ''; $self->update(); } # The one-time build plan has been given, can be used for deciding best way to # show progress # # Looks like: # { # "build_plan": [ # { # "name": "juk", # "phases": [ # "build", # "install" # ] # } # ], # "event": "build_plan" # } sub onBuildPlan { my ($self, $ev) = @_; my (@modules) = @{$ev->{build_plan}}; croak_internal ("Empty build plan!") unless @modules; croak_internal ("Already received a plan!") if exists $self->{planned_phases}; my %num_todo = ( # These are the 'core' phases we expect to be here even with # --no-src, --no-build, etc. update => 0, build => 0, ); my $max_name_width = 0; $self->{planned_phases} = { }; for my $m (@modules) { my @phases = @{$m->{phases}}; $max_name_width = max($max_name_width, length $m->{name}); $num_todo{$_}++ foreach @phases; $self->{planned_phases}->{$m->{name}} = { map { ($_, 'pending') } @phases }; } $self->{done_in_phase}->{$_} = 0 foreach keys %num_todo; $self->{todo_in_phase} = \%num_todo; $self->{max_name_width} = $max_name_width; $self->{build_plan} = $ev->{build_plan}; } # The whole build/install process has completed. sub onBuildDone { my ($self, $ev) = @_; my ($statsRef) = %{$ev->{build_done}}; # --stop-on-failure can cause modules to skip my $numTotalModules = max( map { $self->{todo_in_phase}->{$_} } ( keys %{$self->{todo_in_phase}})); my $numFailedModules = keys %{$self->{failed_at_phase}}; my $numBuiltModules = max( map { $self->{done_in_phase}->{$_} } ( keys %{$self->{done_in_phase}})); my $numSuccesses = $numBuiltModules - $numFailedModules; my $numSkipped = $numTotalModules - $numBuiltModules; my $unicode = ($ENV{LC_ALL} // 'C') =~ /UTF-?8$/; my $happy = $unicode ? '✓' : ':-)'; my $frown = $unicode ? '✗' : ':-('; my $built = $numFailedModules == 0 ? " g[b[$happy] - Built all" : " r[b[$frown] - Worked on"; my $msg = "$built b[$numTotalModules] modules"; if ($numSkipped > 0 || $numFailedModules > 0) { $msg .= " (b[g[$numSuccesses] built OK, b[r[$numFailedModules] failed" if $numFailedModules > 0; $msg .= ", b[$numSkipped] skipped" if $numSkipped > 0; $msg .= ")"; } $self->_clearLineAndUpdate (colorize("$msg\n")); } # The build/install process has forwarded new notices that should be shown. sub onLogEntries { my ($self, $ev) = @_; my ($module, $phase, $entriesRef) = @{$ev->{log_entries}}{qw/module phase entries/}; # Current line may have a transient update msg still, otherwise we're already on # suitable line to print _clearLine() if $self->{last_msg_type} eq 'progress'; if ("$module/$phase" ne $self->{last_mod_entry} && @$entriesRef) { say colorize(" b[y[*] b[$module] $phase:"); $self->{last_mod_entry} = "$module/$phase"; } for my $entry (@$entriesRef) { say $entry; $self->{log_entries}->{$module} //= { build => [ ], update => [ ] }; $self->{log_entries}->{$module}->{$phase} //= [ ]; push @{$self->{log_entries}->{$module}->{$phase}}, $entry; } $self->{last_msg_type} = 'log'; $self->update(); } # TTY helpers sub _checkForBuildPlan { my $self = shift; croak_internal ("Did not receive build plan!") unless keys %{$self->{todo_in_phase}}; } # Generates a string like "update [20/74] build [02/74]" for the requested # phases. sub _progressStringForPhases { my ($self, @phases) = @_; my $result = ''; my $base = ''; foreach my $phase (@phases) { my $cur = $self->{done_in_phase}->{$phase} // 0; my $max = $self->{todo_in_phase}->{$phase} // 0; my $strWidth = length("$max"); my $progress = sprintf("%0*s/$max", $strWidth, $cur); $result .= "$base$phase [$progress]"; $base = ' '; } return $result; } # Generates a string like "update: kcoreaddons build: kconfig" for the # requested phases. You must pass in a hashref mapping each phase name to the # current module name. sub _currentModuleStringForPhases { my ($self, $currentModulesRef, @phases) = @_; my $result = ''; my $base = ''; my $longestNameWidth = $self->{max_name_width}; for my $phase (@phases) { my $curModule = $currentModulesRef->{$phase} // '???'; $curModule .= (' ' x ($longestNameWidth - length ($curModule))); $result .= "$base$phase: $curModule"; $base = ' '; } return $result; } # Returns integer length of the worst-case output line (i.e. the one with a # long module name for each of the given phases). sub _getMinimumOutputWidth { my ($self, @phases) = @_; my $longestName = 'x' x $self->{max_name_width}; my %mockModules = map { ($_, $longestName) } @phases; # fake that the worst-case module is set and find resultant length my $str = $self->_progressStringForPhases(@phases) . " " . $self->_currentModuleStringForPhases(\%mockModules, @phases); return length($str); } sub update { my @phases = qw(update build); my $self = shift; my $term_width = $self->{tty_width}; $self->{min_output} //= $self->_getMinimumOutputWidth(@phases); my $min_width = $self->{min_output}; my $progress = $self->_progressStringForPhases(@phases); my $current_modules = $self->_currentModuleStringForPhases( { update => $self->{cur_update}, build => $self->{cur_working} }, @phases ); my $msg; if ($min_width >= ($term_width - 12)) { # No room for fancy progress, just display what we can $msg = "$progress $current_modules"; } else { my $max_prog_width = ($term_width - $min_width) - 5; my $num_all_done = min(@{$self->{done_in_phase}}{@phases}) // 0; my $num_some_done = max(@{$self->{done_in_phase}}{@phases}, 0) // 0; my $max_todo = max(@{$self->{todo_in_phase}}{@phases}, 1) // 1; my $width = $max_prog_width * $num_all_done / $max_todo; # Leave at least one empty space if we're not fully done $width-- if ($width == $max_prog_width && $num_all_done < $max_todo); my $bar = ('=' x $width); # Show a smaller character entry for updates that are done before the # corresponding build/install. if ($num_some_done > $num_all_done) { $width = $max_prog_width * $num_some_done / $max_todo; $bar .= ('.' x ($width - length ($bar))); } $msg = sprintf("%s [%*s] %s", $progress, -$max_prog_width, $bar, $current_modules); } $self->_clearLineAndUpdate($msg); $self->{last_msg_type} = 'progress'; } sub _clearLine { print "\e[1G\e[K"; } sub _clearLineAndUpdate { my ($self, $msg) = @_; # If last message was a transient progress meter, gives the escape sequence # to return to column 1 and clear the entire line before printing message $msg = "\e[1G\e[K$msg" if $self->{last_msg_type} eq 'progress'; print $msg; STDOUT->flush; $self->{last_msg_type} = 'log'; # update() will change it back if needed } 1;