diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6fecc0ef..e0c8b1a8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -89,7 +89,7 @@ jobs: mingw-w64-x86_64-gmp mingw-w64-x86_64-mpfr mingw-w64-x86_64-zlib - ruby + mingw-w64-x86_64-ruby - name: Download tarball uses: actions/download-artifact@v4 @@ -332,57 +332,34 @@ jobs: mv artifacts/src/*.tar.gz dist mv artifacts/doc-html/*.tar.gz dist mv artifacts/doc-pdf/*.pdf dist - if ls artifacts/*-ubuntu-*/*form >/dev/null 2>&1; then - pkgname=$distname-x86_64-linux - mkdir $pkgname - mv artifacts/*-ubuntu-*/*form $pkgname - chmod +x $pkgname/*form - tar -c $pkgname/* | gzip -c -9 >dist/$pkgname.tar.gz - rm -rf $pkgname - fi - if ls artifacts/*-macos-13/*form >/dev/null 2>&1; then - pkgname=$distname-x86_64-osx - mkdir $pkgname - mv artifacts/*-macos-13/*form $pkgname - chmod +x $pkgname/*form - tar -c $pkgname/* | gzip -c -9 >dist/$pkgname.tar.gz - rm -rf $pkgname - fi - if ls artifacts/*-macos-14/*form >/dev/null 2>&1; then - pkgname=$distname-arm64-osx - mkdir $pkgname - mv artifacts/*-macos-14/*form $pkgname - chmod +x $pkgname/*form - tar -c $pkgname/* | gzip -c -9 >dist/$pkgname.tar.gz - rm -rf $pkgname - fi - # Do not include Windows binaries into the distribution, for now. - # - # if ls artifacts/*-windows-*/*form.exe >/dev/null 2>&1; then - # pkgname=$distname-x86_64-windows - # mkdir $pkgname - # mv artifacts/*-windows-*/*form.exe $pkgname - # chmod +x $pkgname/*form.exe - # # Zip may be more popular than tar.gz for Windows(?) - # # tar -c $pkgname/* | gzip -c -9 >dist/$pkgname.tar.gz - # zip -9 dist/$pkgname.zip $pkgname/* - # rm -rf $pkgname - # fi + make_tar_gz() { + if ls artifacts/$2 >/dev/null 2>&1; then + pkgname=$distname-$1 + mkdir $pkgname + mv artifacts/$2 $pkgname + chmod +x $pkgname/*form* + tar -c $pkgname/* | gzip -c -9 >dist/$pkgname.tar.gz + rm -rf $pkgname + fi + } + make_zip() { + if ls artifacts/$2 >/dev/null 2>&1; then + pkgname=$distname-$1 + mkdir $pkgname + mv artifacts/$2 $pkgname + chmod +x $pkgname/*form* + zip -9 dist/$pkgname.zip $pkgname/* + rm -rf $pkgname + fi + } + make_tar_gz x86_64-linux '*-ubuntu-*/*form' + make_tar_gz x86_64-osx '*-macos-13/*form' + make_tar_gz arm64-osx '*-macos-14/*form' + make_zip x86_64-windows '*-windows-*/*form.exe' - name: Print distributions run: ls -l dist - # Publish the distributions to GitHub Releases, only if the commit has - # a versioning tag. - - name: Publish distributions - if: startsWith(github.ref, 'refs/tags/v') - uses: softprops/action-gh-release@v1 - with: - files: | - dist/*.tar.gz - dist/*.zip - dist/*.pdf - # Upload the distributions as an artifact, regardless of whether # the commit has a versioning tag. This makes checking and debugging easy. - name: Upload distributions as artifacts @@ -394,3 +371,18 @@ jobs: dist/*.zip dist/*.pdf if-no-files-found: error + + # To prevent the distribution of Windows binaries, uncomment the following lines. + # - name: Delete Windows binaries + # run: rm -fv dist/*windows* + + # Publish the distributions to GitHub Releases, only if the commit has + # a versioning tag. + - name: Publish distributions + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v1 + with: + files: | + dist/*.tar.gz + dist/*.zip + dist/*.pdf diff --git a/check/Makefile.am b/check/Makefile.am index 5cec3eb3..6178d805 100644 --- a/check/Makefile.am +++ b/check/Makefile.am @@ -1,12 +1,12 @@ TEST_BINS = if BUILD_FORM -TEST_BINS += $(top_builddir)/sources/form +TEST_BINS += $(top_builddir)/sources/form$(EXEEXT) endif if BUILD_TFORM -TEST_BINS += $(top_builddir)/sources/tform +TEST_BINS += $(top_builddir)/sources/tform$(EXEEXT) endif if BUILD_PARFORM -TEST_BINS += $(top_builddir)/sources/parform +TEST_BINS += $(top_builddir)/sources/parform$(EXEEXT) endif TEST_OPTS = @@ -17,12 +17,12 @@ TESTS_ENVIRONMENT = \ RUBY="$(RUBY)" \ TEST_BINS="$(TEST_BINS)" \ TEST_OPTS="$(TEST_OPTS)" \ - $(SHELL) + "$(SHELL)" TESTS += check-help.sh else TESTS_ENVIRONMENT = \ TEST_BINS="$(TEST_BINS)" \ - $(SHELL) + "$(SHELL)" endif TESTS += benchmark-fu.sh diff --git a/check/check.rb b/check/check.rb index 5e9e1450..bc166a05 100755 --- a/check/check.rb +++ b/check/check.rb @@ -14,9 +14,11 @@ end require "fileutils" +require "io/console/size" require "open3" require "optparse" require "ostruct" +require "rbconfig" require "set" require "thread" require "tmpdir" @@ -72,12 +74,16 @@ def read_env_positive_int(key, default_value) # Get the total size of the physical memory available on the host machine. def get_total_physical_memory - if RUBY_PLATFORM.downcase.include?("linux") + platform = RbConfig::CONFIG["host_os"].downcase + if platform.include?("linux") mem_info = `free -b | grep Mem` mem_info.split[1].to_i - elsif RUBY_PLATFORM.downcase.include?("darwin") + elsif platform.include?("darwin") mem_info = `sysctl -n hw.memsize` mem_info.to_i + elsif platform.include?("mingw") || platform.include?("mswin") + mem_info = `wmic ComputerSystem get TotalPhysicalMemory` + mem_info.split[1].to_i else nil end @@ -168,7 +174,7 @@ def which(name) result = File.expand_path(name) else # Search from $PATH. - ENV["PATH"].split(":").each do |path| + ENV["PATH"].split(File::PATH_SEPARATOR).each do |path| candidate = File.join(path, name) if File.executable?(candidate) result = File.expand_path(candidate) @@ -240,19 +246,24 @@ def wordsize # Host environment. def cygwin? - RUBY_PLATFORM =~ /cygwin/i + RbConfig::CONFIG["host_os"] =~ /cygwin/i end def mac? - RUBY_PLATFORM =~ /darwin/i + RbConfig::CONFIG["host_os"] =~ /darwin|mac os/i end def linux? - RUBY_PLATFORM =~ /linux/i + RbConfig::CONFIG["host_os"] =~ /linux/i end def unix? - cygwin? || mac? || linux? + cygwin? || mac? || linux? || RbConfig::CONFIG["host_os"] =~ /solaris|bsd/i + end + + def windows? + # NOTE: "cygwin" is intentionally excluded. + RbConfig::CONFIG["host_os"] =~ /mswin|msys|mingw|bccwin|wince|emc/i end def travis? @@ -281,6 +292,14 @@ def total_memory @@cached_total_memory = nil @@total_memory_mutex = Mutex.new + def reveal_newlines(str) + if FormTest.cfg.show_newlines + str.gsub(/\r/, "").gsub(/\n/, "\n") + else + str + end + end + # Override methods in Test::Unit::TestCase. def setup @@ -356,6 +375,14 @@ def do_test(&block) break end end + # On Windows, we convert newline characters in stdout/stderr into + # the Unix-style newline characters used in our test cases. + @raw_stdout = @stdout + @raw_stderr = @stderr + if windows? + @stdout = @stdout.gsub(/\r\n/, "\n") + @stderr = @stderr.gsub(/\r\n/, "\n") + end # MesPrint inevitably inserts newline characters when a line exceeds # its length limit. To verify error/warning messages, here we remove # newline characters that seem to be part of continuation lines @@ -379,7 +406,7 @@ def do_test(&block) $stderr.puts("=" * 79) $stderr.puts("#{info.desc} FAILED") $stderr.puts("=" * 79) - $stderr.puts(@stdout) + $stderr.puts(reveal_newlines(@raw_stdout)) $stderr.puts("=" * 79) $stderr.puts if info.status.nil? @@ -397,7 +424,7 @@ def do_test(&block) $stderr.puts("=" * 79) $stderr.puts("#{info.desc} SUCCEEDED") $stderr.puts("=" * 79) - $stderr.puts(@stdout) + $stderr.puts(reveal_newlines(@raw_stdout)) $stderr.puts("=" * 79) $stderr.puts end @@ -585,7 +612,13 @@ def bytesize(exprname, index = -1) def file(filename) begin File.open(File.join(@tmpdir, filename), "r") do |f| - return f.read + result = f.read + # On Windows, we convert newline characters in the file into + # the Unix-style newline characters used in our test cases. + if windows? + result = result.gsub(/\r\n/, "\n") + end + return result end rescue StandardError $stderr.puts("warning: failed to read '#{filename}'") @@ -1088,7 +1121,7 @@ def delete(classname) # FORM configuration. class FormConfig - def initialize(form, mpirun, mpirun_opts, valgrind, valgrind_opts, wordsize, ncpu, timeout, retries, stat, full, verbose) + def initialize(form, mpirun, mpirun_opts, valgrind, valgrind_opts, wordsize, ncpu, timeout, retries, stat, full, verbose, show_newlines) @form = form @mpirun = mpirun @mpirun_opts = mpirun_opts @@ -1100,6 +1133,7 @@ def initialize(form, mpirun, mpirun_opts, valgrind, valgrind_opts, wordsize, ncp @stat = stat @full = full @verbose = verbose + @show_newlines = show_newlines @form_bin = nil @mpirun_bin = nil @@ -1114,7 +1148,7 @@ def initialize(form, mpirun, mpirun_opts, valgrind, valgrind_opts, wordsize, ncp @form_cmd = nil end - attr_reader :form, :mpirun, :mpirun_opts, :valgrind, :valgrind_opts, :ncpu, :timeout, :retries, :stat, :full, :verbose, + attr_reader :form, :mpirun, :mpirun_opts, :valgrind, :valgrind_opts, :ncpu, :timeout, :retries, :stat, :full, :verbose, :show_newlines, :form_bin, :mpirun_bin, :valgrind_bin, :valgrind_supp, :head, :wordsize, :form_cmd def serial? @@ -1131,9 +1165,7 @@ def mpi? def check_bin(name, bin) # Check if the executable is available. - system("cd #{TempDir.root}; type #{bin} >/dev/null 2>&1") - if $? == 0 - # OK. + if File.executable?(bin) return end @@ -1166,7 +1198,8 @@ def check end @head = "" - `#{@form_bin} #{frmname} 2>/dev/null`.split("\n").each do |output_line| + out, _status = Open3.capture2e("#{@form_bin} #{frmname}") + out.split("\n").each do |output_line| if output_line =~ /FORM/ @head = output_line break @@ -1236,14 +1269,16 @@ def check end @form_cmd = cmdlist.join(" ") # Check the output header. - @head = `#{@form_cmd} #{frmname} 2>/dev/null`.split("\n").first - if $? != 0 - system("#{form_cmd} #{frmname}") + out, _err, status = Open3.capture3("#{@form_cmd} #{frmname}") + if status.success? + @head = out.split("\n").first + else fatal("failed to execute '#{@form_cmd}'") end if !@valgrind.nil? # Include valgrind version information. - @head += "\n#{`#{@form_cmd} @{frmname} 2>&1 >/dev/null | grep Valgrind`.split("\n")[0]}" + out, _status = Open3.capture2e("#{@form_cmd} #{frmname}") + @head += "\n" + out.split("\n").select { |line| line.include?("Valgrind") }.first end ensure FileUtils.rm_rf(tmpdir) @@ -1344,6 +1379,7 @@ def main opts.group_count = nil opts.files = [] opts.verbose = false + opts.show_newlines = false parser = OptionParser.new parser.banner = "Usage: #{File.basename($0)} [options] [--] [binname] [files|tests..]" @@ -1387,6 +1423,8 @@ def main "Split tests and run only one group") { |group| opts.group_id, opts.group_count = parse_group(group) } parser.on("-v", "--verbose", "Enable verbose output") { opts.verbose = true } + parser.on("--show-newlines", + "Show newline characters") { opts.show_newlines = true } parser.on("-D TEST=NAME", "Alternative way to run tests NAME") { |pat| opts.name_patterns << parse_def(pat) } begin @@ -1479,7 +1517,7 @@ def main # --path option. if !opts.path.nil? - ENV["PATH"] = "#{opts.path}:#{ENV['PATH']}" + ENV["PATH"] = "#{opts.path}#{File::PATH_SEPARATOR}#{ENV['PATH']}" end # Set FORMPATH @@ -1517,7 +1555,8 @@ def main opts.retries > 1 ? opts.retries : 1, opts.stat, opts.full, - opts.verbose) + opts.verbose, + opts.show_newlines) FormTest.cfg.check puts("Check #{FormTest.cfg.form_bin}") puts(FormTest.cfg.head) @@ -1532,7 +1571,7 @@ def finalize # Print detailed statistics. - term_width = guess_term_width + term_width = IO.console_size[1] max_foldname_width = infos.map { |info| info.foldname.length }.max max_where_width = infos.map { |info| info.where.length }.max + 2 @@ -1628,24 +1667,6 @@ def format_time(time, max_time) format("%s%02d:%02d:%02d.%03d", overflow ? ">" : " ", h, m, s, ms) end -# Return a guessed terminal width. -def guess_term_width - require "io/console" - IO.console.winsize[1] -rescue LoadError, NoMethodError - system("type tput >/dev/null 2>&1") - if $? == 0 - cols = `tput cols 2>/dev/null` - else - cols = ENV["COLUMNS"] || ENV["TERM_WIDTH"] - end - begin - Integer(cols) - rescue ArgumentError, TypeError - 80 - end -end - if $0 == __FILE__ main end diff --git a/check/examples.frm b/check/examples.frm index 6533690a..7aa9ee6e 100644 --- a/check/examples.frm +++ b/check/examples.frm @@ -654,6 +654,7 @@ assert result("G") =~ expr(" Local F = B(1); Print; .end +#pend_if windows? assert finished? assert warning? *--#] Sta_Fill_1 : @@ -1813,6 +1814,7 @@ Local aPLUSbTO3= Print; .end +#require unix? # This gives Valgrind errors (3 memory leaks) on Travis CI # (osx-gcc-valgrind-parvorm), but cleanly works on Linux with mpich 3.2. # Might be an OS- or implementation-specific bug. diff --git a/check/features.frm b/check/features.frm index 05665e7a..5a1ff3c8 100644 --- a/check/features.frm +++ b/check/features.frm @@ -220,7 +220,7 @@ assert result("F2") =~ expr("0") assert result("F3") =~ expr("0") assert result("F4") =~ expr("0") *--#] partitions_ : -*--#[ AppendPath : +*--#[ AppendPath_unix : #include foo/foo1.h * foo/bar/p1.prc #call p1 @@ -238,6 +238,36 @@ P; #call p1 P; .end +#require unix? +#prepare write "foo/foo1.h", "#prependpath bar\n" +#prepare write "foo/foo2.h", "#appendpath bar\n" +#prepare write "foo/bar/p1.prc", "#procedure p1()\nL F=1234;\n#endprocedure\n" +#prepare write "foo/bar/p2.prc", "#procedure p2()\nL G=5678;\n#endprocedure\n" +#prepare write "bar/p1.prc", "#procedure p1()\nL H=9012;\n#endprocedure\n" +assert succeeded? +assert result("F") =~ expr("1234") +assert result("G") =~ expr("5678") +assert result("H") =~ expr("9012") +*--#] AppendPath_unix : +*--#[ AppendPath_windows : +#include foo\foo1.h +* foo/bar/p1.prc +#call p1 +P; +.end +#:path foo;bar +#include foo1.h +* foo/bar/p2.prc +#call p2 +P; +.end +#:path foo;bar +#include foo2.h +* bar/p1.prc +#call p1 +P; +.end +#require windows? #prepare write "foo/foo1.h", "#prependpath bar\n" #prepare write "foo/foo2.h", "#appendpath bar\n" #prepare write "foo/bar/p1.prc", "#procedure p1()\nL F=1234;\n#endprocedure\n" @@ -247,7 +277,7 @@ assert succeeded? assert result("F") =~ expr("1234") assert result("G") =~ expr("5678") assert result("H") =~ expr("9012") -*--#] AppendPath : +*--#] AppendPath_windows : *--#[ TimeoutAfter_1 : #procedure problematicprocedure * Do nothing. @@ -1135,13 +1165,25 @@ Print +s; assert succeeded? assert result("F") =~ expr("0"); *--#] Sortrealloc_2 : -*--#[ TempSortDir : +*--#[ TempSortDir_unix : #: TempSortDir bad/path Local test = 1; .end +#require unix? if mpi? assert runtime_error?("Could not create sort file: bad/path/0formxxx.sor") else assert runtime_error?("Could not create sort file: bad/path/xformxxx.sor") end -*--#] TempSortDir : +*--#] TempSortDir_unix : +*--#[ TempSortDir_windows : +#: TempSortDir bad_path +Local test = 1; +.end +#require windows? +if mpi? + assert runtime_error?('Could not create sort file: bad_path\0formxxx.sor') +else + assert runtime_error?('Could not create sort file: bad_path\xformxxx.sor') +end +*--#] TempSortDir_windows : diff --git a/sources/form3.h b/sources/form3.h index 52553b85..1f3f3926 100644 --- a/sources/form3.h +++ b/sources/form3.h @@ -195,6 +195,7 @@ #define WIN32_LEAN_AND_MEAN #include #include +#include /* Undefine/rename conflicted symbols. */ #undef VOID /* WinNT.h */ #undef MAXLONG /* WinNT.h */ diff --git a/sources/startup.c b/sources/startup.c index 8aafdf03..58e6ade4 100644 --- a/sources/startup.c +++ b/sources/startup.c @@ -804,6 +804,9 @@ classic:; /* Try to create the sort file already, so we can Terminate earlier if this fails. */ +#ifdef WITHPTHREADS + if ( par <= 1 ) { +#endif if ( ( AM.S0->file.handle = CreateFile((char *)AM.S0->file.name) ) < 0 ) { MesPrint("Could not create sort file: %s", AM.S0->file.name); Terminate(-1); @@ -812,6 +815,9 @@ classic:; CloseFile(AM.S0->file.handle); AM.S0->file.handle = -1; remove(AM.S0->file.name); +#ifdef WITHPTHREADS + } +#endif /* With the stage4 and scratch file names we have to be a bit more careful. They are to be allocated after the threads are initialized when there @@ -1624,6 +1630,9 @@ int main(int argc, char **argv) #ifdef TRAPSIGNALS setSignalHandlers(); #endif +#ifdef WINDOWS + _setmode(_fileno(stdout),O_BINARY); +#endif #ifdef WITHPTHREADS AB = ABdummy; @@ -1718,6 +1727,9 @@ VOID CleanUp(WORD par) int i; if ( FG.fname ) { +#ifdef WITHPTHREADS + if ( B ) { +#endif CleanUpSort(0); for ( i = 0; i < 3; i++ ) { if ( AR.Fscr[i].handle >= 0 ) { @@ -1733,6 +1745,9 @@ VOID CleanUp(WORD par) AR.Fscr[i].POfill = 0; } } +#ifdef WITHPTHREADS + } +#endif if ( par > 0 ) { /* Close all input levels above the lowest? @@ -1750,12 +1765,18 @@ VOID CleanUp(WORD par) } } CloseFile(AC.StoreHandle); +#ifdef WITHPTHREADS + if ( B ) +#endif if ( par >= 0 || AR.StoreData.Handle < 0 || AM.ClearStore ) { remove(FG.fname); } dontremove:; #else CloseFile(AC.StoreHandle); +#ifdef WITHPTHREADS + if ( B ) +#endif if ( par >= 0 || AR.StoreData.Handle < 0 || AM.ClearStore > 0 ) { remove(FG.fname); }