diff --git a/MANIFEST b/MANIFEST index 60c1ecacb0f4..a0f66a91a892 100644 --- a/MANIFEST +++ b/MANIFEST @@ -6493,6 +6493,7 @@ t/porting/checkcfgvar.t Check that all config.sh-like files are good t/porting/cmp_version.t Test whether all changed module files have their VERSION bumped t/porting/copyright.t Check that copyright years match t/porting/corelist.t Check that Module-CoreList has perl versions for the current perl +t/porting/cpphdrcheck.t Test headers on the C++ variant of $Config{cc} t/porting/customized.dat Data file for porting/customized.t t/porting/customized.t Check all CUSTOMIZED files are as they should be t/porting/deprecation.t Test that deprecation warnings are handled right diff --git a/t/porting/cpphdrcheck.t b/t/porting/cpphdrcheck.t new file mode 100644 index 000000000000..4f98f56ff686 --- /dev/null +++ b/t/porting/cpphdrcheck.t @@ -0,0 +1,486 @@ +#!perl -w +BEGIN { + chdir "t" if -d "t"; + require './test.pl'; + @INC = "../lib"; +} + +use v5.38; +use Config; +use Cwd "getcwd"; +use File::Temp; +use File::Spec; + +my $cwd = getcwd; +my $devnull = File::Spec->devnull; +my %sources = load_sources(); + +# we chdir around a bit below, which breaks relative paths and Carp +@INC = map File::Spec->rel2abs($_), @INC; + +# the intent is the compiler detection done here will move into a module +# EU::CB doesn't provide what I need here, EU::CppGuess does have some of +# it but isn't core, and has it's own limitations + +my $cc = $Config{cc}; +$cc = shift if @ARGV; + +my $ccpp_cfg = find_ccpp($cc); + +$ccpp_cfg + or skip_all("Cannot find a C++ compiler corresponding to $Config{cc}"); + +$ccpp_cfg->{stdargs}->%* + or skip_all("No standard args found for C++ compiler"); + +note "ccpp: $ccpp_cfg->{ccpp} $ccpp_cfg->{type}"; + +my $headers = <<'HEADERS'; +#include "EXTERN.h" +#include "perl.h" +#include "XSUB.h" + +HEADERS + +my $ccflags = $Config{ccflags}; + +# we add a similar C++ -std +$ccflags =~ s/-std[:=]\S+//; + +$ccflags = "-I" . File::Spec->catdir($cwd, ".."); + +for my $std (sort keys $ccpp_cfg->{stdargs}->%*) { + my $code = get_source("cpp$std"); + $code =~ s(^//PERLHEADERS$)($headers)m or die "Couldn't insert headers in cpp$std"; + ok_compile_only({ code => \$code, std => $std, opts => $ccflags }, $ccpp_cfg, "test std $std"); +} + +done_testing(); + +sub find_ccpp ($cc) { + my $ccpp; + my $cfg; + my $exe = $Config{_exe}; + # gcc + if (($ccpp = $cc) =~ s/\bgcc((?:-\d+)?(?:\Q$exe\E)?)$/g++$1/aa + && ($cfg = check_cpp_compiler($ccpp, "gcc,unix"))) { + return $cfg; + } + # clang + elsif (($ccpp = $cc) =~ s/\bclang((?:-\d+)?(?:\Q$exe\E)?)$/clang++$1/aa + && ($cfg = check_cpp_compiler($ccpp, "clang,unix"))) { + return $cfg; + } + # msvc + # may need work if we ever support clang-cl + elsif ($cc =~ m!([\\/]|^)\bcl(?:(?:\Q$exe\E)?)?$!i + && ($cfg = check_cpp_compiler($cc, "msvc"))) { + return $cfg; + } + else { + # intel C, Sun C + my $ver = `$cc -V 2>$devnull`; + unless ($ver) { + # gcc, clang + $ver = `$cc --version 2>$devnull`; + } + + if ($ver =~ /Intel (?:.*)C(?:\+\+)? Compiler/) { + if (($ccpp = $cc) =~ s/\bicc((?:.exe)?)$/icpc$1/iaa + && ($cfg = check_cpp_compiler($ccpp, "intel,unix"))) { + return $cfg; + } + # icx (Intel oneAPI DPC++/C++ compiler) + elsif (($ccpp = $cc) =~ s/\bicx((?:.exe)?)$/icpx$1/iaa + && ($cfg = check_cpp_compiler($ccpp, "intel,unix"))) { + return $cfg; + } + } + elsif ($ver =~ / Sun .*C/) { + if (($ccpp = $cc) =~ s/\bcc$/CC/aa + && ($cfg = check_cpp_compiler($ccpp, "sunw,unix"))) { + return $cfg; + } + } + # common naming, at least on Linux + if (($ccpp = $cc) =~ s/\b(cc|c89|c99)$/c++/aa) { + my $type = "unix"; # something unix-like + if ($ver =~ /Copyright .* Free Software Foundation/) { + $type = "gcc,unix"; + } + elsif ($ver =~ /clang version/) { + $type = "clang,unix"; + } + if ($cfg = check_cpp_compiler($ccpp, $type)) { + return $cfg; + } + } + } + return undef; +} + +# does a simple check that the supplied compiler can compile C++ +sub check_cpp_compiler ($ccpp, $type = "unix") { + my $ccpp_test_code = <<'CODE'; +#include + +int main() { + std::cout << "OK" << std::endl; + return 0; +} +CODE + my $cfg = + +{ + type => $type, + ccpp => $ccpp, + }; + + note "test run for $ccpp"; + my $out = test_run({ ccpp => $ccpp, code => \$ccpp_test_code }, $cfg); + unless ($out && $out->{stdout} && $out->{stdout} eq "OK\n") { + return; + } + + # see if we can select different C++ standards + # be aware that the default standard varies by compiler and + # version of that compiler + my %std_args; + if ($type eq "msvc") { + # https://learn.microsoft.com/en-us/cpp/build/reference/std-specify-language-standard-version?view=msvc-170 + %std_args = map {; $_ => "-std:c++$_" } qw(14 17 20); + } + elsif ($type eq "sunw") { + # https://docs.oracle.com/cd/E77782_01/html/E77789/bkana.html#OSSCPgnaof + %std_args = map {; $_ => "-std=c++$_" } qw(11 14); + } + elsif ($type =~ /\bunix\b/) { + # Intel + # https://www.intel.com/content/www/us/en/docs/dpcpp-cpp-compiler/developer-guide-reference/2024-1/std-qstd.html + # gcc allows 23 but claims + # "Support is highly experimental, and will almost certainly change in incompatible ways in future releases." + # https://gcc.gnu.org/onlinedocs/gcc/C-Dialect-Options.html + # clang don't document which values are permitted + # https://clang.llvm.org/docs/ClangCommandLineReference.html + %std_args = map {; $_ => "-std=c++$_" } qw(11 14 17 20 23); + } + else { + die "Unknown compiler type $type\n"; + } + + my %stds; + for my $std (sort keys %std_args) { + my $arg = $std_args{$std}; + note "probe $ccpp for standard C++$std with $arg"; + my $code = get_source("cpp$std"); + my $out = test_run({ ccpp => $ccpp, code => \$code, opts => $arg }, $cfg); + if ($out && $out->{stdout} && $out->{stdout} eq "OK\n") { + note "found $std"; + $stds{$std} = $arg; + } + } + $cfg->{stdargs} = \%stds; + + return $cfg; +} + +# perform a test run to see if a compiler works +# $conf can be empty to unix-like defaults, see test_build() for more +sub test_run ($job, $conf) { + my $dir = File::Temp->newdir(); + chdir "$dir" + or die "Cannot chdir to temp directory '$dir': $!"; + my $build = eval { _test_build($job, $conf); } + or note "test_build: $@"; + my $result; + if ($build) { + my $exit = system "$build->{exe} >stdout.txt 2>stderr.txt"; + $result = + +{ + %$build, + exit => $exit, + stdout => scalar _slurp("stdout.txt"), + stderr => scalar _slurp("stderr.txt"), + }; + } + chdir $cwd + or die "Cannot chdir back to '$cwd': $!"; + + $result; +} + +sub ok_compile_only($job, $conf, $name) { + our $Level; + local $Level = $Level + 1; + my $result = _test_compile_only($job, $conf); + + ok($result, $name); +} + +sub _test_compile_only ($job, $conf) { + my $dir = File::Temp->newdir(); + chdir "$dir" + or die "Cannot chdir to temp directory '$dir': $!"; + my $code = $job->{code}; + if (ref $code) { + open my $cfh, ">", "source.cpp" + or die "Cannot create source.cpp: $!"; + print $cfh $$code; + close $cfh + or die "Cannot close source.cpp: $!"; + $code = "source.cpp"; + } + my $opts = $job->{opts} || ''; + $opts = "-c $opts"; + if (my $std = $job->{std}) { + my $std_opt = $conf->{stdargs}{$std} + or die "Unknown standard $std for $conf->{ccpp}\n"; + $opts .= " $std_opt"; + } + + my $cmd = "$conf->{ccpp} $opts $code 2>&1"; + note "running '$cmd'"; + my $out = `$cmd`; + + chdir $cwd; + + unless ($? == 0) { + note "'$cmd' failed: $out"; + return; + } + + return + +{ + out => $out, + }; +} + +# build the supplied code to test we can invoke the compiler +# and so the caller can run it + +sub _test_build ($job, $conf) { + $conf ||= { type => "unix" }; + + my $code = $job->{code}; + if (ref $code) { + open my $cfh, ">", "source.cpp" + or die "Cannot create source.cpp: $!"; + print $cfh $$code; + close $cfh + or die "Cannot close source.cpp: $!"; + $code = "source.cpp"; + } + my $opts = $job->{opts} || ''; + my $_exe = $Config{_exe}; + if ($conf->{type} =~ /\bunix\b/) { + $opts = "-oa.out$_exe $opts"; + } + elsif ($conf->{type} eq "msvc") { + $opts = "/Fea.out$_exe $opts"; + } + else { + die "Unknown type $conf->{type}"; + } + + my $cmd = "$job->{ccpp} $opts $code 2>&1"; + note "running '$cmd'"; + my $out = `$cmd`; + unless ($? == 0) { + note "'$cmd' failed: $out"; + return; + } + + my $exe = "a.out$_exe"; + unless ($^O eq "MSWin32") { + $exe = "./$exe"; + } + return + +{ + exe => $exe, + out => $out, + }; +} + +sub _slurp ($filename) { + open my $fh, "<", $filename + or die "Cannot open $filename: $!"; + return do { local $/; <$fh> }; +} + +sub load_sources { + my %code; + + my $name = ''; + local $_; + while () { + if (/^-- (\w+)$/a) { + $name = $1; + } + elsif ($name) { + $code{$name} .= $_; + } + else { + die "No name seen for code line $_"; + } + } + + return %code; +} + +sub get_source ($keyword) { + $sources{$keyword} + or die "No source found for keyword $keyword\n"; + $sources{$keyword}; +} + +# the test code below tries to use at least one language feature +# specific to that version +__DATA__ +-- cpp11 +#include +#include + +//PERLHEADERS + +struct A { + virtual const char *ok() { return "NOT OK\n"; }; + // = default C++11 + virtual ~A() = default; +}; + +struct B : A { + // override C++11 + const char *ok() override { return "OK\n"; }; +}; + +// unique ptr is C++11 +std::unique_ptr f() { + return std::unique_ptr{new B}; +} + +int main() { + // auto as a placeholder type is C++11 + auto p = f(); + std::cout << p->ok(); + return 0; +} + +-- cpp14 +#include +#include + +//PERLHEADERS + +struct A { + virtual const char *ok() { return "NOT OK\n"; }; + // = default C++11 + virtual ~A() = default; +}; + +struct B : A { + // override C++11 + const char *ok() override { return "OK\n"; }; +}; + +// auto return type is C++14 +auto f() { + return std::unique_ptr{new B{}}; +} + +// deprecated C++14 +[[deprecated]] void g(); + +int main() { + auto p = f(); + // binary literals and ' in numeric literals are C++14 + if (0b100'0000 == 64) + std::cout << p->ok(); + return 0; +} + +-- cpp17 +#include +#include +#include + +//PERLHEADERS + +// for access to sv literals +using namespace std::literals; + +struct A { + // string_view c++17 + virtual std::string_view ok() { return "NOT OK\n"sv; }; + virtual ~A() = default; +}; + +struct B : A { + std::string_view ok() override { return "OK\n"sv; }; +}; + +// [[nodiscard]] is C++17 +[[nodiscard]] auto f() { + return std::unique_ptr{new B{}}; +} + +int main() { + auto p = f(); + // if constexpr C++17 + if constexpr (0b100'0000 == 64) + std::cout << p->ok(); + return 0; +} +-- cpp20 +#include +#include +#include +#include + +//PERLHEADERS + +// for access to sv literals +using namespace std::literals; + +enum class isok { + yes, no +}; + +auto f(isok x) { + // using scoped enum c++20 + using enum isok; + + switch (x) { + case yes: + return "OK\n"sv; + case no: + return "NOT OK\n"sv; + + default: + return "BAD\n"sv; + } +} + +int main() { + std::cout << f(isok::yes); + return 0; +} +-- cpp23 +#include +#include + +//PERLHEADERS + +// for access to sv literals +using namespace std::literals; + +struct A { + // static operator () c++23 + static auto operator()() { + return "OK"sv; + } +}; + +int main() { + // std::println() c++23 + // requires clang trunk or gcc trunk at time of writing + std::println("{}", A{}()); +}