From e2c8fdb64c8a659f1ec9189ea4d17011dda267df Mon Sep 17 00:00:00 2001 From: Fred Hornsey Date: Thu, 18 Apr 2024 15:09:45 -0500 Subject: [PATCH] Post 3.28.0 Release Script Improvements - Fixed an issue with uploading RTPS interop test to the OMG repo. - Update ACE/TAO Workflow: - Try to make it so it won't force push a new commit every time it runs but the PR hasn't been merged yet. - Generate a news fragment so the update will be announced. - Try to improve the commit message and PR title and body so it only says which kind of ACE/TAO being updated. --- .github/workflows/update-ace-tao.yml | 37 ++-- acetao.ini | 6 +- tools/scripts/gitrelease.pl | 199 ++++++++++++++----- tools/scripts/modules/command_utils.pm | 57 ++++-- tools/scripts/modules/misc_utils.pm | 25 ++- tools/scripts/modules/tests/command_utils.pl | 25 ++- 6 files changed, 248 insertions(+), 101 deletions(-) diff --git a/.github/workflows/update-ace-tao.yml b/.github/workflows/update-ace-tao.yml index b96a432e43a..36856311abc 100644 --- a/.github/workflows/update-ace-tao.yml +++ b/.github/workflows/update-ace-tao.yml @@ -31,28 +31,37 @@ jobs: run: | cd OpenDDS GITHUB_TOKEN=${{secrets.GITHUB_TOKEN}} perl tools/scripts/gitrelease.pl --update-ace-tao - # Help make the title and message for commit and PR - perl tools/scripts/modules/ini.pm acetao.ini --join ', ' '.*/version' > ../acevers - echo "ACEVERS=$(cat ../acevers)" >> $GITHUB_ENV - perl tools/scripts/modules/ini.pm acetao.ini --join ', ' '.*/url' > ../acevers_urls - echo "ACEVERS_URLS=$(cat ../acevers_urls)" >> $GITHUB_ENV + prefix="${{GITHUB_WORKSPACE}}/update-ace-tao-" + commit_msg="${prefix}commit.md" + # Only commit and make a PR if the release script recognized a change + # from both master and workflows/update-ace-tao if it exists. + if [ -f "${commit_msg}" ] + then + echo "CREATE_PULL_REQUEST=true" >> $GITHUB_ENV + # create-pull-request can commit for us, but manually commit here so + # we can use file input for the commit message. + # https://github.com/peter-evans/create-pull-request/issues/2864 + git config user.name GitHub + git config user.email noreply@github.com + git add --all + git commit --file "${commit_msg}" \ + --author "${{github.actor}} <${{github.actor}}@users.noreply.github.com>" + echo "PR_TITLE=$(cat ${prefix}pr-title.md)" >> $GITHUB_ENV + echo "PR_BODY_FILE=${prefix}pr-body.md" >> $GITHUB_ENV + else + echo "CREATE_PULL_REQUEST=false" >> $GITHUB_ENV + fi - name: Create Pull Request uses: peter-evans/create-pull-request@v6 + if: ${{env.CREATE_PULL_REQUEST}} id: cpr with: path: OpenDDS token: ${{ secrets.GITHUB_TOKEN }} - commit-message: | - Update ACE/TAO to ${{env.ACEVERS}} - - The releases are ${{env.ACEVERS_URLS}} - committer: GitHub - author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> - signoff: false branch: workflows/update-ace-tao delete-branch: true - title: Update ACE/TAO to ${{env.ACEVERS}} - body: "Releases: ${{env.ACEVERS_URLS}}" + title: ${{env.PR_TITLE}} + body-path: ${{env.PR_BODY_FILE}} labels: | dependencies - name: Check outputs diff --git a/acetao.ini b/acetao.ini index c397421837a..98001480ea0 100644 --- a/acetao.ini +++ b/acetao.ini @@ -1,8 +1,10 @@ # This file contains the common info for ACE/TAO releases. Insead of editing # this directly, run: -# GITHUB_TOKEN=... ./tools/scripts/gitrelease.pl --update-ace-tao +# tools/scripts/gitrelease.pl --update-ace-tao [ace6tao2] +hold=0 +desc=ACE 6/TAO 2 version=6.5.20 repo=https://github.com/DOCGroup/ACE_TAO.git branch=ace6tao2 @@ -18,6 +20,8 @@ tar.bz2-url=https://github.com/DOCGroup/ACE_TAO/releases/download/ACE%2BTAO-6_5_ tar.bz2-md5=2551d7d445456bcb316fe5e9a4b514ea [ace7tao3] +hold=0 +desc=ACE 7/TAO 3 version=7.1.3 repo=https://github.com/DOCGroup/ACE_TAO.git branch=master diff --git a/tools/scripts/gitrelease.pl b/tools/scripts/gitrelease.pl index 4fe6f30137e..4a195bbfa1b 100755 --- a/tools/scripts/gitrelease.pl +++ b/tools/scripts/gitrelease.pl @@ -28,6 +28,7 @@ use command_utils; use ChangeDir; use misc_utils qw/trace/; +use ini qw/read_ini_file/; my $zero_version = parse_version("0.0.0"); @@ -236,6 +237,17 @@ sub run_command { ); } +sub git { + my $args = shift(); + my $exit_statuses = shift() // [0]; + return command_utils::run_command(['git', @{$args}], @_, + script_name => 'gitrelease.pl', + verbose => 1, + autodie => 1, + ignore => $exit_statuses, + ); +} + sub yes_no { my $message = shift; print("$message [y/n] "); @@ -270,6 +282,24 @@ sub touch_file { } } +sub read_file { + my $path = shift(); + + open(my $fh, '<', $path) or trace("Couldn't open file $path: $!"); + my $text = do { local $/; <$fh> }; + close($fh); + + return $text; +} + +sub write_file { + my $path = shift(); + + open(my $fh, '>', $path) or trace("Couldn't open file $path: $!"); + print $fh (@_); + close($fh); +} + my %dummy_release_files = (); sub create_dummy_release_file { @@ -349,22 +379,15 @@ sub check_pithub_result { sub read_json_file { my $path = shift(); - open my $f, '<', $path or trace("Can't open JSON file $path: $!"); - local $/; - my $text = <$f>; - close $f; - - return decode_json($text); + return decode_json(read_file($path)); } sub write_json_file { my $path = shift(); my $what = shift(); - open my $f, '>', $path or trace("Can't open JSON file $path: $!"); # Writes indented, space-separated JSON in a reproducible order - print $f JSON::PP->new->pretty->canonical->encode($what); - close $f; + write_file($path, JSON::PP->new->pretty->canonical->encode($what)); } sub expand_releases { @@ -889,16 +912,17 @@ sub upload_artifacts_from_workflow { ) or trace("Could not download $artifact->{name}"); } - # TODO: This probably shouldn't assume we will always want the first release. - my $release = get_github_releases($settings)->first; - - # Get existing aritfacts in the release + # Get the release of the repo to upload to my $upload_settings = $settings; if (defined($workflow->{pithub_override})) { $upload_settings = new_pithub($settings, %{$workflow->{pithub_override}}, needs_token => 1); } + # TODO: This probably shouldn't assume we will always want the first release. + my $upload_release = get_github_releases($upload_settings)->first; + + # Get existing aritfacts in the release my $assets_ph = $upload_settings->{pithub}->repos->releases->assets; - my $assets_result = $assets_ph->list(release_id => $release->{id}); + my $assets_result = $assets_ph->list(release_id => $upload_release->{id}); check_pithub_result($assets_result); my @existing_assets = (); while (my $existing_asset = $assets_result->next) { @@ -915,7 +939,7 @@ sub upload_artifacts_from_workflow { else { trace("Unexpected artifact name: $artifact->{name}"); } - my $name = $workflow->{arc_name}($settings, $workflow, $os_name); + my $name = $workflow->{arc_name}($upload_settings, $workflow, $os_name); my $dir_path = "$ws_subdir/$name"; if (!-d $dir_path) { mkdir($dir_path) or trace("mkdir $dir_path failed: $!"); @@ -925,7 +949,7 @@ sub upload_artifacts_from_workflow { extract_archive($artifact->{path}, $dir_path) or trace("Extract artifact failed"); - $workflow->{prepare}($settings, $workflow, $os_name, $dir_path); + $workflow->{prepare}($upload_settings, $workflow, $os_name, $dir_path); print("Archiving $name...\n"); my $arc_ext; @@ -942,8 +966,9 @@ sub upload_artifacts_from_workflow { my $filename = "$name$arc_ext"; my $filename_re = quotemeta($filename); if (grep(/^$filename_re$/, @existing_assets)) { - print("Skipping $filename it's already uploaded\n"); - } else { + print("Skipping $filename, it's already uploaded\n"); + } + else { create_archive($dir_path, $arc, no_dir => $workflow->{no_dir}) or trace("Failed to create archive $arc"); push(@to_upload, $arc); @@ -952,12 +977,12 @@ sub upload_artifacts_from_workflow { print("Upload to Github\n"); my %asset_details; - read_assets($settings, \@to_upload, \%asset_details); + read_assets($upload_settings, \@to_upload, \%asset_details); if (defined($workflow->{before_upload})) { $workflow->{before_upload}($upload_settings, $workflow, - \@to_upload, \%asset_details, $release->{id}); + \@to_upload, \%asset_details, $upload_release->{id}); } - github_upload_assets($settings, \@to_upload, \%asset_details, $release->{id}, + github_upload_assets($upload_settings, \@to_upload, \%asset_details, $upload_release->{id}, "\nGithub upload failed, try again"); } @@ -1221,26 +1246,33 @@ sub update_ace_tao { my $doc_repo = $settings->{pithub}->repos->get()->content->{clone_url}; my @arc_exts = ('zip', 'tar.gz', 'tar.bz2'); - my @ace_tao_versions = ( - { - name => 'ace6tao2', - min => parse_version('6.5.0'), - repo => $doc_repo, - branch => 'ace6tao2', - }, - { - name => 'ace7tao3', - min => parse_version('7.1.0'), - repo => $doc_repo, - branch => 'master', - }, - ); + my $ini_path = 'acetao.ini'; + my $update_branch = "workflows/update-ace-tao"; + + # Get the ACE/TAO repos/branches we're interested in + my ($section_names, $sections) = read_ini_file($ini_path); + my @ace_tao_versions; + for my $name (@{$section_names}) { + my $sec = $sections->{$name}; + my $ace_tao_version = { + name => $name, + current => $sec->{version}, + hold => 0, + %{$sec} + }; + if (!$sec->{hold}) { + my $ver = parse_version($sec->{version}); + my $plus = $ver->{minor} + 1; + $ace_tao_version->{next_minor} = parse_version("$ver->{major}.$plus.$ver->{micro}"); + } + push(@ace_tao_versions, $ace_tao_version); + } # Get all the ACE/TAO releases my $release_list = get_github_releases($settings); my @releases = (); while (my $release = $release_list->next) { - next if $release->{prerelease}; + next if ($release->{prerelease}); next if ($release->{tag_name} !~ /^ACE\+TAO-(\d+_\d+_\d+)$/); my $ver = $1; $ver =~ s/_/./g; @@ -1251,15 +1283,26 @@ sub update_ace_tao { } my @sorted = sort { version_cmp($b->{version}, $a->{version}) } @releases; + my @updated; for my $ace_tao_version (@ace_tao_versions) { + print("$ace_tao_version->{name} currently $ace_tao_version->{current}\n"); + if ($ace_tao_version->{hold}) { + print("Hold there for now\n"); + next; + } + # Find versions that match ace_tao_versions. This is the first one that's # less than MAJOR.MINOR+1.MICRO. - my $min = $ace_tao_version->{min}; - my $plus = $min->{minor} + 1; - my $max = parse_version("$min->{major}.$plus.$min->{micro}"); for my $r (@sorted) { - if (version_lesser($r->{version}, $max)) { + if (version_lesser($r->{version}, $ace_tao_version->{next_minor})) { my $version = $r->{version}->{string}; + if ($ace_tao_version->{version} ne $version) { + print("Will update to $ace_tao_version->{version}\n"); + push(@updated, $ace_tao_version); + } + else { + print("$ace_tao_version->{version} is the newest\n"); + } $ace_tao_version->{version} = $version; $ace_tao_version->{url} = $r->{release}->{html_url}; @@ -1298,18 +1341,16 @@ sub update_ace_tao { } # Print the INI file - my $ini_path = 'acetao.ini'; open(my $ini_fh, '>', $ini_path) or trace("Could not open $ini_path: $!"); print $ini_fh ( "# This file contains the common info for ACE/TAO releases. Insead of editing\n", "# this directly, run:\n", - "# GITHUB_TOKEN=... ./tools/scripts/gitrelease.pl --update-ace-tao\n"); + "# tools/scripts/gitrelease.pl --update-ace-tao\n"); for my $ace_tao_version (@ace_tao_versions) { - print $ini_fh ("\n[$ace_tao_version->{name}]\n", - "version=$ace_tao_version->{version}\n", - "repo=$ace_tao_version->{repo}\n", - "branch=$ace_tao_version->{branch}\n", - "url=$ace_tao_version->{url}\n"); + print $ini_fh ("\n[$ace_tao_version->{name}]\n"); + for my $key ('hold', 'desc', 'version', 'repo', 'branch', 'url') { + print $ini_fh ("$key=$ace_tao_version->{$key}\n"); + } for my $ext (@arc_exts) { for my $suffix ('filename', 'url', 'md5') { my $key = "$ext-$suffix"; @@ -1318,6 +1359,65 @@ sub update_ace_tao { } } close($ini_fh); + + if (@updated) { + my $long_desc = "Updated:\n"; + my @versions; + + for my $ace_tao_version (@updated) { + # For Commit/PR Message + push(@versions, $ace_tao_version->{version}); + $long_desc .= "- $ace_tao_version->{desc} from $ace_tao_version->{current} " . + "to [$ace_tao_version->{version}]($ace_tao_version->{url}).\n"; + + # Print news file + write_file("docs/news.d/automated-update-$ace_tao_version->{name}.rst", + "# This file was generated by tools/scripts/gitrelease.pl. It can be edited, but\n", + "# if there is release for $ace_tao_version->{name}, the automated PR will overwrite\n", + "# this file.\n", + ".. news-prs: none\n", + ".. news-start-section: Platform Support and Dependencies\n", + ".. news-start-section: ACE/TAO\n", + "- Updated $ace_tao_version->{desc} from $ace_tao_version->{current} " . + "to `$ace_tao_version->{version} <$ace_tao_version->{url}>`__.\n", + ".. news-end-section\n", + ".. news-end-section\n"); + } + + # Prepare Commit and PR Messages + my $short_desc = 'Update ACE/TAO to ' . join(", ", @versions); + my $full_desc = "$short_desc\n\n$long_desc"; + print($full_desc); + if ($ENV{GITHUB_WORKSPACE}) { + # Write the commit/PR message only if the update branch doesn't already + # exist or the acetao.ini on the update branch is different. We're + # assuming if it's different, then this one is correct and that one is + # out of date. The branch will be forcefully rewritten. + my $file = "file"; + git(['status']); + my $create_pr = 0; + if (git(['ls-remote', '--exit-code', '--heads', + $settings->{remote}, $update_branch], [0, 2])) { + print("$update_branch does not exist\n"); + $create_pr = 1; + } + else { + print("$update_branch exists\n"); + if (git(['--no-pager', 'diff', '--exit-code', + "$settings->{remote}/$update_branch", '--', $ini_path], [0, 1])) { + print("$ini_path on $update_branch differs\n"); + $create_pr = 1; + } + } + + if ($create_pr) { + my $prefix = "$ENV{GITHUB_WORKSPACE}/update-ace-tao-"; + write_file("${prefix}commit.md", $full_desc); + write_file("${prefix}pr-title.md", $short_desc); + write_file("${prefix}pr-body.md", $long_desc); + } + } + } } ############################################################################ @@ -2763,9 +2863,6 @@ sub remedy_github_upload { read_assets($settings, \@assets, \%asset_details); my $releases = $settings->{pithub}->repos->releases; - my $release_notes_path = 'docs/gh-release-notes.md'; - open(my $fh, '<', $release_notes_path) or trace("Failed to read $release_notes_path: $!"); - my $release_notes = do { local $/; <$fh> }; my $release = $releases->create( data => { name => "OpenDDS $settings->{version}", @@ -2773,7 +2870,7 @@ sub remedy_github_upload { body => "**Download $settings->{zip_src} (Windows) or $settings->{tgz_src} (Linux/macOS) " . "instead of \"Source code (zip)\" or \"Source code (tar.gz)\".**\n\n" . - $release_notes, + read_file('docs/gh-release-notes.md'), }, ); check_pithub_result($release); diff --git a/tools/scripts/modules/command_utils.pm b/tools/scripts/modules/command_utils.pm index 5295afd8813..90789ff89fc 100644 --- a/tools/scripts/modules/command_utils.pm +++ b/tools/scripts/modules/command_utils.pm @@ -79,7 +79,8 @@ sub process_capture_arguments { my $ambiguous_dest_type = ref(\$ambiguous_dest); if ($ambiguous_dest_type eq "REF") { $dest_var = $ambiguous_dest; - } else { + } + else { if (defined($ambiguous_dest)) { if (Scalar::Util::openhandle($ambiguous_dest)) { $dest_fh = $ambiguous_dest; @@ -93,7 +94,8 @@ sub process_capture_arguments { if (exists($kw{dump_on_failure}) && $kw{dump_on_failure}) { my $just_for_this_case; $dest_var = \$just_for_this_case; - } else { + } + else { $dest_path = File::Spec->devnull(); } } @@ -158,7 +160,7 @@ sub get_dump_output { # run_command(\@cmd); # # 0 is returned if the command ran and returned a 0 exit status, else 1 is -# returned. +# returned. If autodie is true, then the exit_status is returned instead. # # The optional arguments are passed as a hash key value elements directly # inside the arguments. Example: @@ -180,6 +182,13 @@ sub get_dump_output { # print("Command failed to run\n"); # } # +# ignore +# Array ref of non-zero exit statuses to not treat as errors. Combine with +# exit_status or autodie to get specific exit statuses. +# +# ignore_all +# Do not treat any non-zero exit status as an error. +# # chdir # Change to the given directory before executing the command, returning # afterwards. All other paths passed will be relative to this one. @@ -215,27 +224,28 @@ sub get_dump_output { # }); # # name -# String of the name of the command to report in errors. Defaults to the -# first element of the command array. +# String of the name of the command to report in errors. Defaults to the +# first element of the command array. # # verbose -# Boolean that when true prints the command and current working directory -# before running. Defaults to false. +# Boolean that when true prints the command and current working directory +# before running. Defaults to false. # # dry_run -# The same as verbose, but doesn't run the command. Defaults to false. +# The same as verbose, but doesn't run the command. Defaults to false. # # script_name -# String of the name of the perl script running the command to report in -# errors. Defaults to being blank. +# String of the name of the perl script running the command to report in +# errors. Defaults to being blank. # # error_fh -# File handle to print failure messages and capture dump_on_failure output -# to. Setting to undef disables these. +# File handle to print failure messages and capture dump_on_failure output +# to. Setting to undef disables these. # # autodie -# Die with stack trace if the command failed to run or returned a non-zero -# exit status. Defaults to false. +# Die with stack trace if the command failed to run or returned a non-zero +# exit status that's not ignored. If it doesn't fail, then it returns the +# exit status. Defaults to false. # sub run_command { my $command = shift; @@ -260,6 +270,9 @@ sub run_command { script_name => undef, error_fh => *STDERR, exit_status => undef, + ignore => [], + ignore_all => 0, + ret_exit_status => 0, chdir => undef, autodie => 0, ); @@ -278,6 +291,8 @@ sub run_command { my $script_name = $args{script_name} ? "$args{script_name}: " : ""; my $error_fh = $args{error_fh}; my $exit_status_ref = $args{exit_status}; + my %ignore = map {$_ => 1} @{$args{ignore}}; + my $autodie = $args{autodie}; if ($verbose && defined($verbose_fh)) { my $cwd = getcwd(); @@ -330,6 +345,12 @@ sub run_command { my $system_status = $?; my $system_error = $!; my $ran = $system_status != -1; + my $exit_status = $ran ? $system_status >> 8 : undef; + + # Override failed if we're ignoring this exit status + if ($failed && $ran && ($args{ignore_all} || $ignore{$exit_status})) { + $failed = 0; + } # Reverse redirect for my $capture_directive (@capture_directives) { @@ -347,7 +368,6 @@ sub run_command { } # Process results - my $exit_status = 0; if ($failed) { # Dump output if ran and directed to do so if ($ran) { @@ -359,7 +379,6 @@ sub run_command { } } - $exit_status = $system_status >> 8; my $signal = $system_status & 127; trace("${script_name}\"$name\" was interrupted") if ($signal == SIGINT); my $coredump = $system_status & 128; @@ -378,16 +397,16 @@ sub run_command { print $error_fh "${script_name}ERROR: \"$name\" $error_message\n"; } - if ($args{autodie}) { + if ($autodie) { trace('run_command was set to autodie'); } } if (defined($exit_status_ref)) { - ${$exit_status_ref} = $ran ? $exit_status : undef; + ${$exit_status_ref} = $exit_status; } - return $failed; + return $autodie ? $exit_status : $failed; } 1; diff --git a/tools/scripts/modules/misc_utils.pm b/tools/scripts/modules/misc_utils.pm index b55aa35d3bc..cd2c71a014c 100644 --- a/tools/scripts/modules/misc_utils.pm +++ b/tools/scripts/modules/misc_utils.pm @@ -6,18 +6,33 @@ use warnings; require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw( + get_trace + just_trace trace ); -sub trace { - my $i = 0; - my $msg = "ERROR: " . join('', @_) . "\n"; +sub get_trace { + my $prefix = shift(); + my $offset = shift(); + $prefix = "$prefix: " if ($prefix); + + my $i = $offset; + my $msg = $prefix . join('', @_) . "\n"; while (my @call = (caller($i++))) { my @next = caller($i); my $from = @next ? $next[3] : 'main'; - $msg .= "ERROR: STACK TRACE[" . ($i - 1) . "] $call[1]:$call[2] in $from\n"; + $msg .= $prefix . "STACK TRACE[" . ($i - 1) . "] $call[1]:$call[2] in $from\n"; } - die($msg); + + return $msg; +} + +sub just_trace { + print STDERR (get_trace('ERROR', 1, @_)); +} + +sub trace { + die(get_trace('ERROR', 1, @_)); } 1; diff --git a/tools/scripts/modules/tests/command_utils.pl b/tools/scripts/modules/tests/command_utils.pl index dea9d1b013a..e2c535d772a 100755 --- a/tools/scripts/modules/tests/command_utils.pl +++ b/tools/scripts/modules/tests/command_utils.pl @@ -9,6 +9,7 @@ use FindBin; use lib "$FindBin::RealBin/.."; use command_utils; +use misc_utils qw/just_trace/; my $test_exit_status = 0; @@ -30,19 +31,11 @@ sub check_value { $not_equal = $expected ne $actual; } if (defined($expected) != defined($actual) || $not_equal) { - print STDERR "ERROR: expected $what to be $expected_repr, but it's $actual_repr\n"; + just_trace("expected $what to be $expected_repr, but it's $actual_repr"); $test_exit_status = 1; } } -sub check_boolean { - my $what = shift(); - my $expected = shift() ? "true" : "false"; - my $actual = shift() ? "true" : "false"; - - check_value($what, $expected, $actual); -} - my $cmd_name = 'test-command'; sub run_command { @@ -50,7 +43,7 @@ sub run_command { my $expected_exit_status = shift(); my $exit_status; my $command = shift(); - check_boolean('failure', $expected_failure, + check_value('failure', $expected_failure, command_utils::run_command($command, verbose => 1, error_fh => undef, @@ -71,8 +64,18 @@ sub perl { if ($^O ne 'MSWin32') { run_command(1, undef, '___this_really_should_be_invalid___'); } -run_command(1, 2, perl('exit(2);')); run_command(0, 0, perl('exit(0);')); +run_command(1, 1, perl('exit(1);')); +run_command(1, 2, perl('exit(2);')); + +print("Check that ignore_exit_statues works ==========================================\n"); +run_command(0, 0, perl('exit(0);'), ignore => [1]); +run_command(0, 1, perl('exit(1);'), ignore => [1]); +run_command(1, 2, perl('exit(2);'), ignore => [1]); +run_command(0, 0, perl('exit(0);'), ignore => [2], autodie => 1); +eval { run_command(0, 1, perl('exit(1);'), ignore => [2], autodie => 1); }; +check_value('died', 1, $@ ? 1 : 0); +run_command(2, 2, perl('exit(2);'), ignore => [2], autodie => 1); print("Check that putting the ouput in a variable works ==============================\n"); my $stdout;