Skip to content

Commit 5a401a5

Browse files
committed
Add source hash comparator & refactor download lock
1 parent 8fbe6ee commit 5a401a5

File tree

10 files changed

+140
-35
lines changed

10 files changed

+140
-35
lines changed

src/SPC/builder/LibraryBase.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public function setup(bool $force = false): int
5151
// if source is locked as pre-built, we just tryInstall it
5252
$pre_built_name = Downloader::getPreBuiltLockName($source);
5353
if (isset($lock[$pre_built_name]) && ($lock[$pre_built_name]['lock_as'] ?? SPC_DOWNLOAD_SOURCE) === SPC_DOWNLOAD_PRE_BUILT) {
54-
return $this->tryInstall($lock[$pre_built_name]['filename'], $force);
54+
return $this->tryInstall($lock[$pre_built_name], $force);
5555
}
5656
return $this->tryBuild($force);
5757
}
@@ -166,14 +166,15 @@ public function getBinaryFiles(): array
166166
* @throws WrongUsageException
167167
* @throws FileSystemException
168168
*/
169-
public function tryInstall(string $install_file, bool $force_install = false): int
169+
public function tryInstall(array $lock, bool $force_install = false): int
170170
{
171+
$install_file = $lock['filename'];
171172
if ($force_install) {
172173
logger()->info('Installing required library [' . static::NAME . '] from pre-built binaries');
173174

174175
// Extract files
175176
try {
176-
FileSystem::extractPackage($install_file, DOWNLOAD_PATH . '/' . $install_file, BUILD_ROOT_PATH);
177+
FileSystem::extractPackage($install_file, $lock['source_type'], DOWNLOAD_PATH . '/' . $install_file, BUILD_ROOT_PATH);
177178
$this->install();
178179
return LIB_STATUS_OK;
179180
} catch (FileSystemException|RuntimeException $e) {
@@ -183,19 +184,19 @@ public function tryInstall(string $install_file, bool $force_install = false): i
183184
}
184185
foreach ($this->getStaticLibs() as $name) {
185186
if (!file_exists(BUILD_LIB_PATH . "/{$name}")) {
186-
$this->tryInstall($install_file, true);
187+
$this->tryInstall($lock, true);
187188
return LIB_STATUS_OK;
188189
}
189190
}
190191
foreach ($this->getHeaders() as $name) {
191192
if (!file_exists(BUILD_INCLUDE_PATH . "/{$name}")) {
192-
$this->tryInstall($install_file, true);
193+
$this->tryInstall($lock, true);
193194
return LIB_STATUS_OK;
194195
}
195196
}
196197
// pkg-config is treated specially. If it is pkg-config, check if the pkg-config binary exists
197198
if (static::NAME === 'pkg-config' && !file_exists(BUILD_ROOT_PATH . '/bin/pkg-config')) {
198-
$this->tryInstall($install_file, true);
199+
$this->tryInstall($lock, true);
199200
return LIB_STATUS_OK;
200201
}
201202
return LIB_STATUS_ALREADY;

src/SPC/command/DeleteDownloadCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public function handle(): int
6060

6161
foreach ($deleted_sources as $lock_name) {
6262
// remove download file/dir if exists
63-
if ($lock[$lock_name]['source_type'] === 'archive') {
63+
if ($lock[$lock_name]['source_type'] === SPC_SOURCE_ARCHIVE) {
6464
if (file_exists($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock[$lock_name]['filename']))) {
6565
logger()->info('Deleting file ' . $path);
6666
unlink($path);

src/SPC/doctor/item/LinuxMuslCheck.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public function fixMusl(): bool
7878
];
7979
logger()->info('Downloading ' . $musl_source['url']);
8080
Downloader::downloadSource($musl_version_name, $musl_source);
81-
FileSystem::extractSource($musl_version_name, DOWNLOAD_PATH . "/{$musl_version_name}.tar.gz");
81+
FileSystem::extractSource($musl_version_name, SPC_SOURCE_ARCHIVE, DOWNLOAD_PATH . "/{$musl_version_name}.tar.gz");
8282

8383
// Apply CVE-2025-26519 patch
8484
SourcePatcher::patchFile('musl-1.2.5_CVE-2025-26519_0001.patch', SOURCE_PATH . "/{$musl_version_name}");

src/SPC/store/Downloader.php

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ public static function downloadFile(string $name, string $url, string $filename,
208208
if ($download_as === SPC_DOWNLOAD_PRE_BUILT) {
209209
$name = self::getPreBuiltLockName($name);
210210
}
211-
self::lockSource($name, ['source_type' => 'archive', 'filename' => $filename, 'move_path' => $move_path, 'lock_as' => $download_as]);
211+
self::lockSource($name, ['source_type' => SPC_SOURCE_ARCHIVE, 'filename' => $filename, 'move_path' => $move_path, 'lock_as' => $download_as]);
212212
}
213213

214214
/**
@@ -231,6 +231,9 @@ public static function lockSource(string $name, array $data): void
231231
} else {
232232
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true) ?? [];
233233
}
234+
// calculate hash
235+
$hash = self::getLockSourceHash($data);
236+
$data['hash'] = $hash;
234237
$lock[$name] = $data;
235238
FileSystem::writeFile(DOWNLOAD_PATH . '/.lock.json', json_encode($lock, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
236239
}
@@ -278,7 +281,7 @@ public static function downloadGit(string $name, string $url, string $branch, ?s
278281
}
279282
// Lock
280283
logger()->debug("Locking git source {$name}");
281-
self::lockSource($name, ['source_type' => 'dir', 'dirname' => $name, 'move_path' => $move_path, 'lock_as' => $lock_as]);
284+
self::lockSource($name, ['source_type' => SPC_SOURCE_GIT, 'dirname' => $name, 'move_path' => $move_path, 'lock_as' => $lock_as]);
282285

283286
/*
284287
// 复制目录过去
@@ -371,6 +374,16 @@ public static function downloadPackage(string $name, ?array $pkg = null, bool $f
371374
SPC_DOWNLOAD_PRE_BUILT
372375
);
373376
break;
377+
case 'local':
378+
// Local directory, do nothing, just lock it
379+
logger()->debug("Locking local source {$name}");
380+
self::lockSource($name, [
381+
'source_type' => SPC_SOURCE_LOCAL,
382+
'dirname' => $pkg['dirname'],
383+
'move_path' => $pkg['extract'] ?? null,
384+
'lock_as' => SPC_DOWNLOAD_PACKAGE,
385+
]);
386+
break;
374387
case 'custom': // Custom download method, like API-based download or other
375388
$classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\store\source');
376389
foreach ($classes as $class) {
@@ -477,6 +490,16 @@ public static function downloadSource(string $name, ?array $source = null, bool
477490
$download_as
478491
);
479492
break;
493+
case 'local':
494+
// Local directory, do nothing, just lock it
495+
logger()->debug("Locking local source {$name}");
496+
self::lockSource($name, [
497+
'source_type' => SPC_SOURCE_LOCAL,
498+
'dirname' => $source['dirname'],
499+
'move_path' => $source['extract'] ?? null,
500+
'lock_as' => $download_as,
501+
]);
502+
break;
480503
case 'custom': // Custom download method, like API-based download or other
481504
if (isset($source['func']) && is_callable($source['func'])) {
482505
$source['name'] = $name;
@@ -594,6 +617,43 @@ public static function getPreBuiltLockName(string $source): string
594617
return "{$source}-" . PHP_OS_FAMILY . '-' . getenv('GNU_ARCH') . '-' . (getenv('SPC_LIBC') ?: 'default') . '-' . (SystemUtil::getLibcVersionIfExists() ?? 'default');
595618
}
596619

620+
/**
621+
* Get the hash of the lock source based on the lock options.
622+
*
623+
* @param array $lock_options Lock options
624+
* @return string Hash of the lock source
625+
* @throws RuntimeException
626+
*/
627+
public static function getLockSourceHash(array $lock_options): string
628+
{
629+
$result = match ($lock_options['source_type']) {
630+
SPC_SOURCE_ARCHIVE => sha1_file(DOWNLOAD_PATH . '/' . $lock_options['filename']),
631+
SPC_SOURCE_GIT => exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $lock_options['dirname']) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD'),
632+
SPC_SOURCE_LOCAL => 'LOCAL HASH IS ALWAYS DIFFERENT',
633+
default => filter_var(getenv('SPC_IGNORE_BAD_HASH'), FILTER_VALIDATE_BOOLEAN) ? '' : throw new RuntimeException("Unknown source type: {$lock_options['source_type']}"),
634+
};
635+
if ($result === false && !filter_var(getenv('SPC_IGNORE_BAD_HASH'), FILTER_VALIDATE_BOOLEAN)) {
636+
throw new RuntimeException("Failed to get hash for source: {$lock_options['source_type']}");
637+
}
638+
return $result ?: '';
639+
}
640+
641+
/**
642+
* @param array $lock_options Lock options
643+
* @param string $destination Target directory
644+
* @throws FileSystemException
645+
* @throws RuntimeException
646+
*/
647+
public static function putLockSourceHash(array $lock_options, string $destination): void
648+
{
649+
$hash = self::getLockSourceHash($lock_options);
650+
if ($lock_options['source_type'] === SPC_SOURCE_LOCAL) {
651+
logger()->debug("Source [{$lock_options['dirname']}] is local, no hash will be written.");
652+
return;
653+
}
654+
FileSystem::writeFile("{$destination}/.spc-hash", $hash);
655+
}
656+
597657
/**
598658
* Register CTRL+C event for different OS.
599659
*
@@ -640,8 +700,8 @@ private static function isAlreadyDownloaded(string $name, bool $force, int $down
640700
// If lock file exists, skip downloading for source mode
641701
if (!$force && $download_as === SPC_DOWNLOAD_SOURCE && isset($lock[$name])) {
642702
if (
643-
$lock[$name]['source_type'] === 'archive' && file_exists(DOWNLOAD_PATH . '/' . $lock[$name]['filename']) ||
644-
$lock[$name]['source_type'] === 'dir' && is_dir(DOWNLOAD_PATH . '/' . $lock[$name]['dirname'])
703+
$lock[$name]['source_type'] === SPC_SOURCE_ARCHIVE && file_exists(DOWNLOAD_PATH . '/' . $lock[$name]['filename']) ||
704+
$lock[$name]['source_type'] === SPC_SOURCE_GIT && is_dir(DOWNLOAD_PATH . '/' . $lock[$name]['dirname'])
645705
) {
646706
logger()->notice("Source [{$name}] already downloaded: " . ($lock[$name]['filename'] ?? $lock[$name]['dirname']));
647707
return true;
@@ -652,8 +712,8 @@ private static function isAlreadyDownloaded(string $name, bool $force, int $down
652712
if (!$force && $download_as === SPC_DOWNLOAD_PRE_BUILT && isset($lock[$lock_name = self::getPreBuiltLockName($name)])) {
653713
// lock name with env
654714
if (
655-
$lock[$lock_name]['source_type'] === 'archive' && file_exists(DOWNLOAD_PATH . '/' . $lock[$lock_name]['filename']) ||
656-
$lock[$lock_name]['source_type'] === 'dir' && is_dir(DOWNLOAD_PATH . '/' . $lock[$lock_name]['dirname'])
715+
$lock[$lock_name]['source_type'] === SPC_SOURCE_ARCHIVE && file_exists(DOWNLOAD_PATH . '/' . $lock[$lock_name]['filename']) ||
716+
$lock[$lock_name]['source_type'] === SPC_SOURCE_GIT && is_dir(DOWNLOAD_PATH . '/' . $lock[$lock_name]['dirname'])
657717
) {
658718
logger()->notice("Pre-built content [{$name}] already downloaded: " . ($lock[$lock_name]['filename'] ?? $lock[$lock_name]['dirname']));
659719
return true;

src/SPC/store/FileSystem.php

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public static function copyDir(string $from, string $to): void
142142
* @throws RuntimeException
143143
* @throws FileSystemException
144144
*/
145-
public static function extractPackage(string $name, string $filename, ?string $extract_path = null): void
145+
public static function extractPackage(string $name, string $source_type, string $filename, ?string $extract_path = null): void
146146
{
147147
if ($extract_path !== null) {
148148
// replace
@@ -151,14 +151,15 @@ public static function extractPackage(string $name, string $filename, ?string $e
151151
} else {
152152
$extract_path = PKG_ROOT_PATH . '/' . $name;
153153
}
154-
logger()->info("extracting {$name} package to {$extract_path} ...");
154+
logger()->info("Extracting {$name} package to {$extract_path} ...");
155155
$target = self::convertPath($extract_path);
156156

157157
if (!is_dir($dir = dirname($target))) {
158158
self::createDir($dir);
159159
}
160160
try {
161-
self::extractArchive($filename, $target);
161+
// extract wrapper command
162+
self::extractWithType($source_type, $filename, $extract_path);
162163
} catch (RuntimeException $e) {
163164
if (PHP_OS_FAMILY === 'Windows') {
164165
f_passthru('rmdir /s /q ' . $target);
@@ -177,24 +178,23 @@ public static function extractPackage(string $name, string $filename, ?string $e
177178
* @throws FileSystemException
178179
* @throws RuntimeException
179180
*/
180-
public static function extractSource(string $name, string $filename, ?string $move_path = null): void
181+
public static function extractSource(string $name, string $source_type, string $filename, ?string $move_path = null): void
181182
{
182183
// if source hook is empty, load it
183184
if (self::$_extract_hook === []) {
184185
SourcePatcher::init();
185186
}
186-
if ($move_path !== null) {
187-
$move_path = SOURCE_PATH . '/' . $move_path;
188-
} else {
189-
$move_path = SOURCE_PATH . "/{$name}";
190-
}
187+
$move_path = match ($move_path) {
188+
null => SOURCE_PATH . '/' . $name,
189+
default => self::isRelativePath($move_path) ? (SOURCE_PATH . '/' . $move_path) : $move_path,
190+
};
191191
$target = self::convertPath($move_path);
192-
logger()->info("extracting {$name} source to {$target}" . ' ...');
192+
logger()->info("Extracting {$name} source to {$target}" . ' ...');
193193
if (!is_dir($dir = dirname($target))) {
194194
self::createDir($dir);
195195
}
196196
try {
197-
self::extractArchive($filename, $target);
197+
self::extractWithType($source_type, $filename, $move_path);
198198
self::emitSourceExtractHook($name, $target);
199199
} catch (RuntimeException $e) {
200200
if (PHP_OS_FAMILY === 'Windows') {
@@ -484,11 +484,6 @@ public static function replaceFileLineContainsString(string $file, string $find,
484484
*/
485485
private static function extractArchive(string $filename, string $target): void
486486
{
487-
// Git source, just move
488-
if (is_dir(self::convertPath($filename))) {
489-
self::copyDir(self::convertPath($filename), $target);
490-
return;
491-
}
492487
// Create base dir
493488
if (f_mkdir(directory: $target, recursive: true) !== true) {
494489
throw new FileSystemException('create ' . $target . ' dir failed');
@@ -553,4 +548,16 @@ private static function emitSourceExtractHook(string $name, string $target): voi
553548
}
554549
}
555550
}
551+
552+
private static function extractWithType(string $source_type, string $filename, string $extract_path): void
553+
{
554+
logger()->debug('Extracting source [' . $source_type . ']: ' . $filename);
555+
/* @phpstan-ignore-next-line */
556+
match ($source_type) {
557+
SPC_SOURCE_ARCHIVE => self::extractArchive($filename, $extract_path),
558+
SPC_SOURCE_GIT => self::copyDir(self::convertPath($filename), $extract_path),
559+
// soft link to the local source
560+
SPC_SOURCE_LOCAL => symlink(self::convertPath($filename), $extract_path),
561+
};
562+
}
556563
}

src/SPC/store/PackageManager.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ public static function installPackage(string $pkg_name, ?array $config = null, b
3434
Downloader::downloadPackage($pkg_name, $config, $force);
3535
// After download, read lock file name
3636
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true);
37+
$source_type = $lock[$pkg_name]['source_type'];
3738
$filename = DOWNLOAD_PATH . '/' . ($lock[$pkg_name]['filename'] ?? $lock[$pkg_name]['dirname']);
3839
$extract = $lock[$pkg_name]['move_path'] === null ? (PKG_ROOT_PATH . '/' . $pkg_name) : $lock[$pkg_name]['move_path'];
39-
FileSystem::extractPackage($pkg_name, $filename, $extract);
40+
FileSystem::extractPackage($pkg_name, $source_type, $filename, $extract);
4041

4142
// if contains extract-files, we just move this file to destination, and remove extract dir
4243
if (is_array($config['extract-files'] ?? null) && is_assoc_array($config['extract-files'])) {

src/SPC/store/SourceManager.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,39 @@ public static function initSource(?array $sources = null, ?array $libs = null, ?
6969
$check = $lock[$lock_name]['move_path'] === null ? (SOURCE_PATH . '/' . $source) : (SOURCE_PATH . '/' . $lock[$lock_name]['move_path']);
7070
if (!is_dir($check)) {
7171
logger()->debug('Extracting source [' . $source . '] to ' . $check . ' ...');
72-
FileSystem::extractSource($source, DOWNLOAD_PATH . '/' . ($lock[$lock_name]['filename'] ?? $lock[$lock_name]['dirname']), $lock[$lock_name]['move_path']);
72+
$filename = self::getSourceFullPath($lock[$lock_name]);
73+
FileSystem::extractSource($source, $lock[$lock_name]['source_type'], $filename, $lock[$lock_name]['move_path']);
74+
Downloader::putLockSourceHash($lock[$lock_name], $check);
75+
continue;
76+
}
77+
// if a lock file does not have hash, calculate with the current source (backward compatibility)
78+
if (!isset($lock[$lock_name]['hash'])) {
79+
$hash = Downloader::getLockSourceHash($lock[$lock_name]);
7380
} else {
81+
$hash = $lock[$lock_name]['hash'];
82+
}
83+
84+
// when source already extracted, detect if the extracted source hash is the same as the lock file one
85+
if (file_exists("{$check}/.spc-hash") && FileSystem::readFile("{$check}/.spc-hash") === $hash) {
7486
logger()->debug('Source [' . $source . '] already extracted in ' . $check . ', skip !');
87+
continue;
7588
}
89+
90+
// if not, remove the source dir and extract again
91+
logger()->notice("Source [{$source}] hash mismatch, removing old source dir and extracting again ...");
92+
FileSystem::removeDir($check);
93+
$filename = self::getSourceFullPath($lock[$lock_name]);
94+
FileSystem::extractSource($source, $lock[$lock_name]['source_type'], $filename, $lock[$lock_name]['move_path']);
95+
Downloader::putLockSourceHash($lock[$lock_name], $check);
7696
}
7797
}
98+
99+
private static function getSourceFullPath(array $lock_options): string
100+
{
101+
return match ($lock_options['source_type']) {
102+
SPC_SOURCE_ARCHIVE => FileSystem::isRelativePath($lock_options['filename']) ? (DOWNLOAD_PATH . '/' . $lock_options['filename']) : $lock_options['filename'],
103+
SPC_SOURCE_GIT, SPC_SOURCE_LOCAL => FileSystem::isRelativePath($lock_options['dirname']) ? (DOWNLOAD_PATH . '/' . $lock_options['dirname']) : $lock_options['dirname'],
104+
default => throw new WrongUsageException("Unknown source type: {$lock_options['source_type']}"),
105+
};
106+
}
78107
}

src/globals/defines.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
'zendopcache' => 'opcache',
4141
];
4242

43-
// spc lock type
43+
// spc download lock type
4444
const SPC_DOWNLOAD_SOURCE = 1; // lock source
4545
const SPC_DOWNLOAD_PRE_BUILT = 2; // lock pre-built
4646
const SPC_DOWNLOAD_PACKAGE = 3; // lock as package
@@ -84,5 +84,10 @@
8484
const AUTOCONF_LDFLAGS = 8;
8585
const AUTOCONF_ALL = 15;
8686

87+
// spc download source type
88+
const SPC_SOURCE_ARCHIVE = 'archive'; // download as archive
89+
const SPC_SOURCE_GIT = 'git'; // download as git repository
90+
const SPC_SOURCE_LOCAL = 'local'; // download as local directory
91+
8792
ConsoleLogger::$date_format = 'H:i:s';
8893
ConsoleLogger::$format = '[%date%] [%level_short%] %body%';

0 commit comments

Comments
 (0)