33declare(strict_types=1);
38 const PACKAGE =
'littlegreenviper/lgv_tz_lookup';
43 $download = $download ?? [self::class,
'download'];
44 $download(
'https://api.github.com/repos/evansiroky/timezone-boundary-builder/releases/latest',
$directory.
'/release.json');
45 $release = json_decode(file_get_contents(
$directory.
'/release.json'),
true, 512, JSON_THROW_ON_ERROR);
46 if (!is_array($release) || !is_string($release[
'tag_name'] ??
null) || trim($release[
'tag_name']) ===
'') {
47 throw new RuntimeException(
'The latest boundary release has no version tag.');
49 foreach ($release[
'assets'] ?? [] as $asset) {
50 if (($asset[
'name'] ??
'') ===
'timezones-with-oceans.geojson.zip' &&
51 is_string($asset[
'browser_download_url'] ??
null) &&
52 str_starts_with($asset[
'browser_download_url'],
'https://')) {
53 return [
'version' => $release[
'tag_name'],
'asset' => $asset];
56 throw new RuntimeException(
'The latest release has no full oceans GeoJSON archive.');
62 $size = filesize($archive);
63 if ($size ===
false || $size === 0 || (isset($asset[
'size']) && $size !== $asset[
'size'])) {
64 throw new RuntimeException(
'The boundary archive is empty or incomplete.');
66 $checksum = hash_file(
'sha256', $archive);
67 if (!empty($asset[
'digest']) && str_starts_with($asset[
'digest'],
'sha256:') &&
68 !hash_equals(substr($asset[
'digest'], 7), $checksum)) {
69 throw new RuntimeException(
'The boundary archive checksum differs from release metadata.');
79 $output = fopen($destination,
'wb');
80 if (
false === $output) {
81 throw new RuntimeException(
'Could not write '.$destination);
83 $request = curl_init($url);
85 curl_setopt_array($request, [
86 CURLOPT_FILE => $output,
87 CURLOPT_FOLLOWLOCATION =>
true,
88 CURLOPT_MAXREDIRS => 5,
89 CURLOPT_CONNECTTIMEOUT => 30,
90 CURLOPT_TIMEOUT => 1800,
91 CURLOPT_FAILONERROR =>
true,
92 CURLOPT_USERAGENT =>
'LGV-TZ-Lookup-Setup',
93 CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
94 CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS,
96 if (!curl_exec($request)) {
97 throw new RuntimeException(
'Download failed: '.curl_error($request).
' ('.$url.
')');
107 public static function extract(
string $archive,
110 $zip = new ZipArchive();
111 if (
true !== $zip->open($archive)) {
112 throw new RuntimeException(
'Could not open the boundary archive.');
116 for ($i = 0; $i < $zip->numFiles; ++$i) {
117 $name = $zip->getNameIndex($i);
118 if (preg_match(
'/\.(?:json|geojson)$/i', $name)) {
122 if (count($entries) !== 1) {
123 throw new RuntimeException(
'Expected one GeoJSON file in the boundary archive.');
126 $input = $zip->getStream($entries[0]);
127 $output = fopen($destination,
'wb');
128 if (
false === $input ||
false === $output) {
129 if (is_resource($input)) { fclose($input); }
130 if (is_resource($output)) { fclose($output); }
131 throw new RuntimeException(
'Could not extract the boundary file.');
134 $bytes = stream_copy_to_stream($input, $output);
135 $entry = $zip->statName($entries[0]);
136 if (
false === $bytes || $bytes === 0 || $bytes !== $entry[
'size']) {
137 throw new RuntimeException(
'The extracted boundary file is empty or incomplete.');
151 echo
"Installing a Composer application...\n";
153 self::download(
'https://getcomposer.org/download/latest-stable/composer.phar', $composer);
154 self::download(
'https://getcomposer.org/download/latest-stable/composer.phar.sha256sum',
$directory.
'/composer.sha256');
155 $checksum = preg_split(
'/\s+/', trim(file_get_contents(
$directory.
'/composer.sha256')))[0];
156 if (!hash_equals($checksum, hash_file(
'sha256', $composer))) {
157 throw new RuntimeException(
'Composer download checksum did not match.');
159 $repository = [
'type' =>
'vcs',
'url' =>
'https://github.com/LittleGreenViper/LGV_TZ_Lookup.git'];
160 if ($packagePath !==
'') {
161 $packagePath = realpath($packagePath);
162 if (
false === $packagePath || !is_file($packagePath.
'/composer.json')) {
163 throw new RuntimeException(
'The package path must point to a checkout containing composer.json.');
165 $version =
'dev-local';
166 $repository = [
'type' =>
'path',
'url' => $packagePath,
167 'options' => [
'symlink' =>
false,
'versions' => [self::PACKAGE => $version]]];
169 file_put_contents(
$directory.
'/composer.json', json_encode([
170 'name' =>
'littlegreenviper/tz-lookup-application',
171 'repositories' => [$repository],
172 'require' => [self::PACKAGE => $version,
'ext-pdo_'.
$driver =>
'*',
'salsify/json-streaming-parser' =>
'8.3.*'],
173 ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR).PHP_EOL);
174 putenv(
'COMPOSER_HOME='.
$directory.
'/composer-home');
175 putenv(
'COMPOSER_CACHE_DIR='.
$directory.
'/composer-cache');
176 putenv(
'COMPOSER_ROOT_VERSION=1.0.0');
178 PHP_BINARY, $composer,
'install',
'--working-dir='.
$directory,
'--no-dev',
'--no-interaction',
179 '--no-scripts',
'--no-plugins',
'--prefer-dist',
'--no-progress',
'--no-ansi',
'--quiet',
180 ], [0 => [
'file',
'/dev/null',
'r'], 1 => STDOUT, 2 => STDERR], $pipes);
182 throw new RuntimeException(
'Could not start Composer.');
188 throw new RuntimeException(
'Composer installation failed (exit '.$status.
').');
197 echo
'Installed '.self::PACKAGE.
' '.Composer\InstalledVersions::getPrettyVersion(self::PACKAGE).
".\n";
This prepares a Composer application and its boundary data.
static install(string $directory, string $driver, string $packagePath='', string $version='^1.4')
Install the library and optional loader, then require its Composer autoloader.
static extract(string $archive, string $destination)
Extract exactly one GeoJSON entry without using archive paths.
static latestBoundaries(string $directory, ?callable $download=null)
Discover the latest full boundary release with oceans.
static verifyBoundaries(string $archive, array $asset)
Verify the archive against the release's size and optional SHA-256 digest.
static download(string $url, string $destination)
Download an HTTPS resource to a local file.
const PACKAGE
The package installed by both command-line tools.
if(!defined( '__CONFIG_FILE_'))