The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

#!env perl
use strict;
use File::chdir;
use File::Basename qw/basename dirname/;
use File::Find qw/find/;
use File::Path qw/make_path remove_tree/;
use File::Copy qw/copy/;
use File::Copy::Recursive qw/dircopy/;
use File::Temp qw/tempfile/;
use IPC::Run qw/run/;
use Perl::OSType qw/is_os_type/;
use POSIX qw/EXIT_SUCCESS/;
autoflush STDOUT 1;
# ------------------------------------------------------------
# Generation of objects using perl setup, for use in perl's XS
# ------------------------------------------------------------
my $version = get_version();
print "Generating config for tconv version $version\n";
# ------------------------
# Write configuration file
# ------------------------
my $config_h_in = File::Spec->catfile('include', 'tconv_config.h.in');
my $config_h = File::Spec->catfile('output', 'include', 'tconv_config.h');
make_path(dirname($config_h));
my $ac = Config::AutoConf::INI->new(logfile => 'config.log');
$ac->define_var('TCONV_NTRACE', 1);
$ac->check;
write_tconv_config($version, $config_h_in, $config_h);
# -------------
# Fake export.h
# -------------
my $export_h = File::Spec->catfile('output', 'include', 'tconv', 'export.h');
make_path(dirname($export_h));
open(my $fh, '>', $export_h) || die "Cannot open $export_h, $!";
print $fh "#define tconv_EXPORT\n";
print $fh "#define TCONV_NO_EXPORT\n";
close($fh) || warn "Cannot close $export_h, $!";
my @additional_includes = ();
my @additional_srcdir = ();
my $extra_compiler_flags = '';
# -----------------------------------------------------
# We depend on libiconv that is in the form of a tar.gz
# -----------------------------------------------------
{
my $tar=Archive::Tar->new();
my $libiconv_tar = File::Spec->catfile('3rdparty', 'tar', 'libiconv-1.15.tar.gz');
$tar->read($libiconv_tar);
print "... Extracting $libiconv_tar in 3rdparty/output\n";
make_path(File::Spec->catdir('3rdparty', 'output'));
{
local $CWD = File::Spec->catdir('3rdparty', 'output');
$tar->extract();
}
undef $tar;
#
# We take care to rename config.h to iconv_config.h everwhere in libiconv
#
find(
{
no_chdir => 1,
wanted => sub {
my $file = File::Spec->canonpath($_);
if (-f $file && ($file =~ /\.c$/ || $file =~ /\.h$/)) {
open(my $in, '<', $file) || die "Failed to open $file, $!";
my $content = do { local $/; <$in>; };
close($in) || warn "Failed to close $file, $!";
my $orig = $content;
$content =~ s/"config\.h"/"iconv_config.h"/g;
$content =~ s/<config\.h>/<iconv_config.h>/g;
if ($orig ne $content) {
open(my $out, '>', $file) || die "Failed to open $file, $!";
print $out $content;
close($out) || warn "Failed to close $file, $!";
print STDERR "... Replaced config.h by iconv_config.h in $file\n";
}
}
},
},
File::Spec->catdir('3rdparty', 'output', 'libiconv-1.15'));
my $config_h = File::Spec->catfile('3rdparty', 'output', 'libiconv-1.15', 'include', 'iconv_config.h');
print "... Generate file $config_h\n";
open(my $fd, '>', $config_h) || die "Failed to open $config_h, $!";
print $fd "
/* Generated file */
/* localcharset.c will play itself with WIN32_LEAN_AND_MEAN... */
#undef WIN32_LEAN_AND_MEAN
\n";
close($fd) || warn "Failed to close $config_h, $!";
my $iconv_h_in = File::Spec->catfile('3rdparty', 'output', 'libiconv-1.15', 'include', 'iconv.h.in');
my $iconv_h = File::Spec->catfile('3rdparty', 'output', 'libiconv-1.15', 'include', 'iconv.h');
print "... Generate file $iconv_h\n";
write_iconv_h($iconv_h_in, $iconv_h);
my $localcharset_h_in = File::Spec->catfile('3rdparty', 'output', 'libiconv-1.15', 'libcharset', 'include', 'localcharset.h.in');
my $localcharset_h = File::Spec->catfile('3rdparty', 'output', 'libiconv-1.15', 'libcharset', 'include', 'localcharset.h');
print "... Generate file $localcharset_h\n";
write_localcharset_h($localcharset_h_in, $localcharset_h);
print "... Checking for nl_langinfo(CODESET)\n";
$extra_compiler_flags .= " -DHAVE_LANGINFO_CODESET" if get_have_langinfo_codeset();
my $have_decl_getc_unlocked;
$ac->check_func('getc_unlocked', { # and not check_decl
action_on_false => sub { $have_decl_getc_unlocked = 0 },
action_on_true => sub { $have_decl_getc_unlocked = 1 } } );
$extra_compiler_flags .= " -DHAVE_DECL_GETC_UNLOCK" if $have_decl_getc_unlocked;
print "... Checking for endianness\n";
$extra_compiler_flags .= " -DWORDS_LITTLEENDIAN" unless get_is_big_endian();
$extra_compiler_flags .= " -DICONV_CONST=";
$extra_compiler_flags .= " -DLIBDIR=\\\"\\\"";
$extra_compiler_flags .= " -DHAVE_WORKING_O_NOFOLLOW=0";
$extra_compiler_flags .= " -DENABLE_EXTRA";
push(@additional_srcdir, File::Spec->catfile('3rdparty', 'output', 'libiconv-1.15', 'libcharset', 'lib', 'localcharset.c'));
push(@additional_srcdir, File::Spec->catfile('3rdparty', 'output', 'libiconv-1.15', 'lib', 'relocatable.c'));
push(@additional_srcdir, File::Spec->catfile('3rdparty', 'output', 'libiconv-1.15', 'lib', 'iconv.c'));
push(@additional_includes, File::Spec->catfile('3rdparty', 'output', 'libiconv-1.15', 'include'));
push(@additional_includes, File::Spec->catfile('3rdparty', 'output', 'libiconv-1.15', 'libcharset', 'include'));
}
# -----------------------------------------------------
# We depend on cchardet that is in the form of a tar.gz
# -----------------------------------------------------
{
my $tar=Archive::Tar->new();
my $cchardet_tar = File::Spec->catfile('3rdparty', 'tar', 'cchardet-1.0.0.tar.gz');
$tar->read($cchardet_tar);
print "... Extracting $cchardet_tar in 3rdparty/output\n";
make_path(File::Spec->catdir('3rdparty', 'output'));
{
local $CWD = File::Spec->catdir('3rdparty', 'output');
$tar->extract();
}
undef $tar;
#
# nspr-emu mess, we fix it by generating ourself what is needed
#
my $libcharsetdetect = File::Spec->catdir('3rdparty', 'output', 'cchardet-1.0.0', 'src', 'ext', 'libcharsetdetect');
my $nspremu = File::Spec->catdir($libcharsetdetect, 'nspr-emu');
print "... Suppress directory $nspremu\n";
remove_tree($nspremu);
print "... Generate directory $nspremu\n";
make_path($nspremu);
#
# nsDebug.h.in have no dependency on cmake discoveries
#
my $nsDebug = File::Spec->catfile($nspremu, 'nsDebug.h');
print "... Generate file $nsDebug\n";
copy(File::Spec->catfile('include', 'nsDebug.h.in'), $nsDebug);
#
# prmem.h.in have no dependency on cmake discoveries
#
my $prmem = File::Spec->catfile($nspremu, 'prmem.h');
print "... Generate file $prmem\n";
copy(File::Spec->catfile('include', 'prmem.h.in'), $prmem);
#
# nscore.h.in depend on some type sizes
#
my $nscore = File::Spec->catfile($libcharsetdetect, 'nscore.h');
print "... Suppress file $nscore\n";
unlink($nscore);
print "... Generate file $nscore\n";
write_nscore(File::Spec->catfile('include', 'nscore.h.in'), $nscore);
push(@additional_srcdir, File::Spec->catdir('3rdparty', 'output', 'cchardet-1.0.0', 'src', 'ext', 'libcharsetdetect', 'mozilla', 'extensions', 'universalchardet', 'src', 'base'));
push(@additional_srcdir, File::Spec->catfile($libcharsetdetect, 'charsetdetect.cpp'));
push(@additional_includes, File::Spec->catdir($libcharsetdetect, 'mozilla', 'extensions', 'universalchardet', 'src', 'base'));
push(@additional_includes, File::Spec->catdir($libcharsetdetect, 'nspr-emu'));
push(@additional_includes, $libcharsetdetect);
}
# -----------------------------------
# On Windows, we will use dlfcn-win32
# -----------------------------------
my @additional = ();
if (is_os_type('Windows')) {
push(@additional, 'dlfcn-win32');
#
# This depend on psapi, and perl's cflags always include that by default
#
push(@additional_srcdir, File::Spec->catdir('output', '3rdparty', 'dlfcn-win32', 'dlfcn.c'));
#
# Well, dlfcn.h is at dlfcn-win32 level
#
push(@additional_includes, File::Spec->catdir('output', '3rdparty', 'dlfcn-win32'));
#
}
# --------------------------------------------------------------------------------
# getopt is only used for the tconv binary - not needed from library point of view
# --------------------------------------------------------------------------------
# -----------------------------------------------------------------
# We depend on these* stuff that can manage themselvess, eventually
# -----------------------------------------------------------------
foreach (qw/genericLogger/, @additional) {
if (! dircopy(File::Spec->catdir('3rdparty', 'github', $_),
File::Spec->catdir('output', '3rdparty', $_))) {
die "Failed to copy $_";
}
print "... Copying $_\n";
my $CMakeObjects = File::Spec->catfile('output', '3rdparty', $_, 'CMakeObjects.PL');
if (-e $CMakeObjects) {
print "... Executing $CMakeObjects\n";
my $in;
my $out;
my $err;
{
local $CWD = dirname($CMakeObjects);
run([$^X, basename($CMakeObjects)], \$in, \$out, \$err) || die "Failed to execute $CMakeObjects" . ($err ? "\n$err" : '');
}
if ($out) {
foreach (grep {defined} split(/\R/, $out)) {
print "... ... $_\n";
}
}
my $inc = File::Spec->catdir('output', '3rdparty', $_, 'output', 'include');
push(@additional_includes, $inc) if (-d $inc);
}
my $inc = File::Spec->catdir('output', '3rdparty', $_, 'include');
push(@additional_includes, $inc) if (-d $inc);
}
$ac->write_config_h(File::Spec->catfile('output', 'include', 'tconv', 'config_autoconf.h'));
# ----------------
# Get source files
# ----------------
my @sources;
find(
{
no_chdir => 1,
wanted => sub {
my $file = File::Spec->canonpath($_);
if (-f $file && ($file =~ /\.c$/ || $file =~ /\.cpp$/)) {
#
# We know that *ICU.c are exceptions
#
my $bfile = basename($file);
if (! ($file =~ /ICU\.c$/)) {
push(@sources, $file)
}
}
},
},
'src', @additional_srcdir);
# ----------------------------------------------------------------------------------------
# Generate objects
# (individually- not efficient but I do not see how CBuilder mixes C and C++ source files)
# ----------------------------------------------------------------------------------------
my $cbuilder = ExtUtils::CBuilder->new();
my @objects;
my $obj_dir = File::Spec->catfile('output', 'obj4perl');
make_path($obj_dir);
foreach my $source (@sources) {
my $is_cplusplus = ($source =~ /\.cpp$/i || $source =~ /\.c\+\+$/i);
my $obj = File::Spec->catfile($obj_dir, basename($cbuilder->object_file($source)));
push(@objects, $cbuilder->object_file($source));
$cbuilder->compile(
source => $source,
extra_compiler_flags => $extra_compiler_flags,
object_file => $obj,
include_dirs => [ 'include', File::Spec->catdir('output', 'include'), @additional_includes ],
'C++' => $is_cplusplus
);
}
# ----
# Done
# ----
exit(EXIT_SUCCESS);
sub get_version {
open(my $fh, '<', 'CMakeLists.txt') || die "Cannot open CMakeLists.txt, $!";
my $content = do { local $/; <$fh>; };
close($fh) || warn "Failed to close CMakeLists.txt, $!";
my @rc;
if ($content =~ /^MYPACKAGESTART\s*\(\s*tconv\s+(\d+)\s+(\d+)\s+(\d+)\s*\)/sm) {
@rc = ($1, $2, $3);
} else {
foreach (qw/TCONV_VERSION_MAJOR TCONV_VERSION_MINOR TCONV_VERSION_PATCH/) {
if ($content =~ /^SET\s*\(\s*$_\s*(\d+)\s*\)/sm) {
push(@rc, $1);
} else {
die "Failed to find $_",
}
}
}
return join('.', @rc)
}
sub write_tconv_config {
my ($version, $input, $output) = @_;
make_path(dirname($output));
open(my $fh, '<', $input) || die "Cannot open $input, $!";
my $source = do { local $/; <$fh>; };
close($fh) || warn "Cannot close $input, $!";
$source =~ s/^[ \t]*#[ \t]*cmakedefine[ \t]+(\w+)+[ \t]+\@([^ \t@]*)\@//smg;
open($fh, '>', $output) || die "Cannot open $output, $!";
print $fh <<CONFIG;
#ifndef TCONV_CONFIG_WRAPPER_H
#define TCONV_CONFIG_WRAPPER_H
#include <tconv/config_autoconf.h>
#define TCONV_C_INLINE
#define TCONV_HAVE_ICONV 1
#define TCONV_VERSION "$version"
$source
#endif /* TCONV_CONFIG_WRAPPER_H */
CONFIG
close($fh) || warn "Cannot close $output, $!";
}
sub write_nscore {
my ($input, $output) = @_;
make_path(dirname($output));
open(my $fh, '<', $input) || die "Cannot open $input, $!";
my $source = do { local $/; <$fh>; };
close($fh) || warn "Cannot close $input, $!";
$source =~ s/^[ \t]*#[ \t]*cmakedefine\b.*?$//smg;
$source =~ s/^[ \t]*#[ \t]*define\s+SIZEOF.*?$//smg;
open($fh, '>', $output) || die "Cannot open $output, $!";
print $fh <<NSCORE;
#ifndef NSCORE_WRAPPER_H
#define NSCORE_WRAPPER_H
#include <tconv/config_autoconf.h>
$source
#endif /* TCONV_CONFIG_WRAPPER_H */
NSCORE
close($fh) || warn "Cannot close $output, $!";
}
sub write_iconv_h {
my ($input, $output) = @_;
make_path(dirname($output));
open(my $fh, '<', $input) || die "Cannot open $input, $!";
my $source = do { local $/; <$fh>; };
close($fh) || warn "Cannot close $input, $!";
$source =~ s/\@DLL_VARIABLE\@//smg;
$source =~ s/\@EILSEQ\@/134679/smg;
$source =~ s/\@ICONV_CONST\@//smg;
$source =~ s/\@USE_MBSTATE_T\@/0/smg;
$source =~ s/\@HAVE_WCHAR_T\@/0/smg;
open($fh, '>', $output) || die "Cannot open $output, $!";
print $fh $source;
close($fh) || warn "Cannot close $output, $!";
}
sub write_localcharset_h {
my ($input, $output) = @_;
make_path(dirname($output));
open(my $fh, '<', $input) || die "Cannot open $input, $!";
my $source = do { local $/; <$fh>; };
close($fh) || warn "Cannot close $input, $!";
open($fh, '>', $output) || die "Cannot open $output, $!";
print $fh $source;
close($fh) || warn "Cannot close $output, $!";
}
sub get_have_langinfo_codeset {
my $source .= q'
#include <stdlib.h>
#include <langinfo.h>
int main(int ac, char **av)
{
char *cs = nl_langinfo(CODESET);
exit(0);
}';
my $rc = eval {
my $cbuilder = ExtUtils::CBuilder->new();
my ($fh, $filename) = tempfile(SUFFIX => '.c', UNLINK => 0);
print $fh $source;
close($fh) || warn "Cannot close $filename, $!";
my $obj = $cbuilder->object_file($filename);
$cbuilder->compile(
source => $filename,
object_file => $obj,
'C++' => 0
);
$cbuilder->link_executable(
module_name => 'ICONV::TEST',
objects => $obj
);
system($cbuilder->exe_file($obj), 'unused', 'parameter');
my $exitcode = $? >> 8;
unlink $cbuilder->exe_file($obj);
unlink $obj;
unlink $filename;
$exitcode ? 0 : 1;
} // 0;
print $@ if $@;
return $rc;
}
sub get_is_big_endian {
my $source .= q'
#include <stdlib.h>
const int i = 1;
#define is_bigendian() ( (*(char*)&i) == 0 )
int main(int ac, char **av)
{
exit(is_bigendian() ? 0 : 1);
}';
my $cbuilder = ExtUtils::CBuilder->new();
my ($fh, $filename) = tempfile(SUFFIX => '.c', UNLINK => 0);
print $fh $source;
close($fh) || warn "Cannot close $filename, $!";
my $obj = $cbuilder->object_file($filename);
$cbuilder->compile(
source => $filename,
object_file => $obj,
'C++' => 0
);
$cbuilder->link_executable(
module_name => 'ICONV::TEST',
objects => $obj
);
system($cbuilder->exe_file($obj), 'unused', 'parameter');
my $exitcode = $? >> 8;
unlink $cbuilder->exe_file($obj);
unlink $obj;
my $rc = $exitcode ? 0 : 1;
unlink $filename;
return $rc;
}