DateTime-Lite

 view release on metacpan or  search on metacpan

scripts/build_tz_database.pl  view on Meta::CPAN

#!/usr/bin/env perl
##----------------------------------------------------------------------------
## DateTime::Lite::TimeZone - ~/scripts/build_tz_database.pl
## Version v0.4.0
## Copyright(c) 2026 DEGUEST Pte. Ltd.
## Author: Jacques Deguest <jack@deguest.jp>
## Created 2026/04/03
## Modified 2026/04/07
## All rights reserved
##
## This program is free software; you can redistribute  it  and/or  modify  it
## under the same terms as Perl itself.
##----------------------------------------------------------------------------
# SYNOPSIS
#   # Fetch latest tzdata from IANA, compile, build database:
#   perl scripts/build_tz_database.pl [--verbose|--debug 3]
#
#   # Use a specific version (fetched from IANA if not cached):
#   perl scripts/build_tz_database.pl --tz-version 2026a [--verbose|--debug 3]
#
#   # Use already-downloaded tarball:
#   perl scripts/build_tz_database.pl --tarball /path/to/tzdata2026a.tar.gz
#
#   # Use system zoneinfo directory (no download):
#   perl scripts/build_tz_database.pl --zoneinfo /usr/share/zoneinfo
#
# DESCRIPTION
#   Builds the SQLite timezone database bundled with DateTime::Lite::TimeZone.
#
#   Primary mode: downloads the latest (or specified) tzdata release from
#   IANA (https://ftp.iana.org/tz/releases/), verifies the GPG signature,
#   compiles the Olson source files with zic(1), then reads the resulting
#   TZif binary files (RFC 8536). Downloaded tarballs are cached under
#   ~/.cache/dtl-tzdata/ to avoid redundant downloads.
#
#   Fallback mode (--zoneinfo): reads TZif files from a local zoneinfo
#   directory instead of downloading. Useful when IANA is not reachable.
#
# REQUIREMENTS
#   Always:       zic(1), Perl 5.10.1+, DBD::SQLite >= 1.27, DBI >= 1.611
#   IANA mode:    HTTP::Promise or Net::FTP depending on --proto (default to 'http')
#   Recommended:  gpg(1) for signature verification
#   Optional:     rdfind(1), symlinks(1) for zoneinfo deduplication
#
# SEE ALSO
#   Repository on Github: <https://github.com/eggert/tz>
##----------------------------------------------------------------------------
use v5.10.1;
use strict;
use warnings;
use Config;
use Data::Pretty qw( dump );
use DBI ();
use Encode ();
use File::Which qw( which );
use Getopt::Class;
use JSON;
use Module::Generic::File qw( cwd file stdout stderr tempdir );
use Pod::Usage;
use POSIX qw( strftime );
use Term::ANSIColor::Simple;
our $VERSION   = 'v0.4.0';
our $LOG_LEVEL = 0;
our $DEBUG     = 0;
our $VERBOSE   = 0;

# NOTE: Constants
use constant NEG_INF_SENTINEL =>  -9_223_372_036_854_775_807;
use constant POS_INF_SENTINEL =>   9_223_372_036_854_775_807;

# Seconds from Rata Die epoch (0001-01-01) to Unix epoch (1970-01-01)
# Verified: DateTime->new(year=>1970,month=>1,day=>1,time_zone=>'UTC')->utc_rd_as_seconds
use constant UNIX_TO_RD => 62_135_683_200;

use constant IANA_RELEASES    => 'https://ftp.iana.org/tz/releases';
# We need both code and data to compile the binaries and tzdata.zi
use constant IANA_LATEST_CODE => 'https://ftp.iana.org/tz/tzcode-latest.tar.gz';
use constant IANA_LATEST_DATA => 'https://ftp.iana.org/tz/tzdata-latest.tar.gz';

# Olson source files to compile with zic, in the conventional order
use constant OLSON_FILES => [qw(
    africa
    antarctica
    asia
    australasia
    europe
    northamerica
    southamerica
    etcetera
    factory
    backward
)];

use constant TZINFO_EXTRA_FILES => [qw(
    iso3166.tab
    zone1970.tab
    zonenow.tab
    zone.tab
    tzdata.zi
)];

our $HAS_NATIVE_I64 = 0;

scripts/build_tz_database.pl  view on Meta::CPAN

            $json->encode( $ref );
        } || die( "Unable to encode array to JSON for array values @$ref: $@" );
        return( $encoded );
    }
}

sub _tzif_data_block_size
{
    my( $h, $time_size ) = @_;

    return(
        $h->{timecnt} * $time_size +
        $h->{timecnt} +
        $h->{typecnt} * 6 +
        $h->{charcnt} +
        $h->{leapcnt} * ( $time_size + 4 ) +
        $h->{isstdcnt} +
        $h->{isutcnt}
    );
}

sub _u8
{
    my( $s ) = @_;
    return( unpack( 'C', $s ) );
}

sub _u32be
{
    my( $s ) = @_;
    return( unpack( 'N', $s ) );
}

# _version_from_tarball( $tarball )
# Extracts the tzdata version string from the Makefile inside the tarball.
sub _version_from_tarball
{
    my $tarball = file( shift( @_ ) );

    my $tar = _has_tool('tar') ||
        die( "Unable to find tar on your system." );
    # VERSION= line in the Makefile
    my $makefile = qx( $tar -xOf "$tarball" Makefile 2>/dev/null ) // '';
    if( $makefile =~ /^VERSION[[:blank:]\h]*=[[:blank:]\h]*(\S+)/m )
    {
        return( $1 );
    }

    # Fallback: parse from the tarball filename
    my $basename = $tarball->basename;
    if( $basename =~ /tzdata(\d{4}[a-z]+)\.tar\.gz/ )
    {
        return( $1 );
    }

    die( "Cannot determine tzdata version from '$tarball'.\n" );
}

# _verify_signature( $tarball )
# Verifies the GPG signature $tarball.asc against $tarball.
# Non-fatal if gpg is absent or the key cannot be fetched.
sub _verify_signature
{
    my $tarball = shift( @_ );
    my $asc     = file( "$tarball.asc" );
    _message( 2, "Verifying tarball signature with <green>$asc</>" );

    my $gpg = _has_tool('gpg');
    unless( $gpg )
    {
        warn( "  gpg not found; skipping signature verification.\n" );
        return;
    }
    unless( $asc->exists )
    {
        warn( "  Signature file '$asc' not found; skipping verification.\n" );
        return;
    }

    # Try to ensure the IANA signing key is available.
    # Paul Eggert's current key: ED97E90E62AA7E34
    my $key_id = 'ED97E90E62AA7E34';
    unless( qx( gpg --list-keys "$key_id" 2>/dev/null ) =~ /$key_id/i )
    {
        _message( 1, "  Importing IANA signing key $key_id..." );
        system( $gpg,
            '--keyserver', 'keys.openpgp.org',
            '--recv-keys', $key_id
        ) == 0 || warn( "Warning only: error calling $gpg to import IANA public key: exit ", ( $? >> 8 ) );
        # Non-fatal: key servers may be unreachable
    }

    _message( 1, "  Verifying GPG signature with: $gpg --verify $asc $tarball" );
    if( system( $gpg, '--verify', $asc, $tarball ) )
    {
        warn( "  GPG signature verification FAILED for '$tarball'.\n"
            . "  Proceeding; verify manually if this concerns you.\n" );
    }
    else
    {
        _message( 1, "  Signature OK." );
    }
}

__END__

=encoding utf8

=head1 NAME

build_tz_database.pl - Build the DateTime::Lite::TimeZone SQLite database

=head1 SYNOPSIS

    # Fetch latest tzdata from IANA, compile, build database:
    perl scripts/build_tz_database.pl [--verbose]

    # Specific version:
    perl scripts/build_tz_database.pl --tz-version 2026a

    # Already-downloaded tarball:
    perl scripts/build_tz_database.pl --tarball /path/to/tzdata2026a.tar.gz

    # Use system zoneinfo (no download):
    perl scripts/build_tz_database.pl --zoneinfo /usr/share/zoneinfo

=head1 DESCRIPTION

Builds the SQLite timezone database bundled with L<DateTime::Lite::TimeZone>.

B<Primary mode> downloads the latest (or specified) tzdata release directly from IANA (L<https://ftp.iana.org/tz/releases/>), verifies the GPG signature, compiles the Olson source files with C<zic(1)>, and parses the resulting TZif binary files (RFC ...

B<Fallback mode> (C<--zoneinfo>) reads TZif files from a local compiled zoneinfo directory instead of downloading. Useful when IANA is unreachable or for quick local rebuilds from the installed system timezone data.

Run this script whenever a new tzdata release is available, then commit the updated C<lib/DateTime/Lite/tz.sqlite3>.

=head1 OPTIONS

=over 4

=item C<--tz-version> I<version>

Target a specific tzdata version such as C<2026a>. Defaults to the latest version found on the IANA releases page.

=item C<--tarball> I<file>

Use an already-downloaded C<tzdata*.tar.gz> file, skipping the download.

=item C<--zoneinfo> I<directory>

Use a local compiled zoneinfo directory instead of downloading from IANA.

=item C<--db> I<file>

Output database path. Defaults to C<lib/DateTime/Lite/tz.sqlite3> relative to the distribution root.

=item C<--cache-dir> I<directory>

Where to store cached tarballs. Defaults to C<~/.cache/dtl-tzdata/>.

=item C<--skip-verify>

Skip GPG signature verification. Not recommended for production.

=item C<--verbose>

Print one line per timezone as it is processed.

=back

=head1 REQUIREMENTS

Always required: C<zic(1)> (from the C<tzdata> or C<tz-utils> system package), L<DBI>, L<DBD::SQLite> >= 1.27.

For primary mode: C<curl(1)> or C<wget(1)>.

Recommended: C<gpg(1)> for signature verification.

Optional (non-fatal if absent): C<rdfind(1)> for deduplication, C<symlinks(1)> for relative symlink conversion.

=head1 EPOCH CONVERSION

TZif transition times are stored as seconds since the Unix epoch (1970-01-01T00:00:00 UTC). L<DateTime> uses seconds since the Rata Die epoch (0001-01-01T00:00:00). The constant C<UNIX_TO_RD = 62_135_683_200> converts between them.

=head1 AUTHOR

Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>

=head1 COPYRIGHT & LICENSE

Copyright(c) 2026 DEGUEST Pte. Ltd.

All rights reserved

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

=cut



( run in 0.626 second using v1.01-cache-2.11-cpan-e1769b4cff6 )