static-php-cli/src/StaticPHP/Util/LicenseDumper.php
crazywhalecc 274098b3a6
Merge remote-tracking branch 'origin/v3-refactor/libs' into v3-refactor/libs
# Conflicts:
#	config/artifact.yml
#	config/pkg.lib.yml
#	spc.registry.json
2026-02-03 19:14:53 +08:00

271 lines
9.7 KiB
PHP

<?php
declare(strict_types=1);
namespace StaticPHP\Util;
use StaticPHP\Artifact\Artifact;
use StaticPHP\Config\ArtifactConfig;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Registry\ArtifactLoader;
/**
* License dumper for v3, dumps artifact license files to target directory
*/
class LicenseDumper
{
/** @var array<string> Artifact names to dump */
private array $artifacts = [];
/**
* Add artifacts by name.
*
* @param array<string> $artifacts Artifact names
*/
public function addArtifacts(array $artifacts): self
{
$this->artifacts = array_unique(array_merge($this->artifacts, $artifacts));
return $this;
}
/**
* Dump all collected artifact licenses to target directory.
*
* @param string $target_dir Target directory path
* @return bool True on success
*/
public function dump(string $target_dir): bool
{
// Create target directory if not exists (don't clean existing files)
if (!is_dir($target_dir)) {
FileSystem::createDir($target_dir);
} else {
logger()->debug("Target directory exists, will append/update licenses: {$target_dir}");
}
$license_summary = [];
$dumped_count = 0;
foreach ($this->artifacts as $artifact_name) {
$artifact = ArtifactLoader::getArtifactInstance($artifact_name);
if ($artifact === null) {
logger()->warning("Artifact not found, skipping: {$artifact_name}");
continue;
}
try {
$result = $this->dumpArtifactLicense($artifact, $target_dir, $license_summary);
if ($result) {
++$dumped_count;
}
} catch (\Throwable $e) {
logger()->warning("Failed to dump license for {$artifact_name}: {$e->getMessage()}");
}
}
// Generate SUMMARY.json (read-modify-write)
$this->generateSummary($target_dir, $license_summary);
logger()->info("Successfully dumped {$dumped_count} license(s) to: {$target_dir}");
return true;
}
/**
* Dump license for a single artifact.
*
* @param Artifact $artifact Artifact instance
* @param string $target_dir Target directory
* @param array &$license_summary Summary data to populate
* @return bool True if dumped
* @throws SPCInternalException
*/
private function dumpArtifactLicense(Artifact $artifact, string $target_dir, array &$license_summary): bool
{
$artifact_name = $artifact->getName();
// Get metadata from ArtifactConfig
$artifact_config = ArtifactConfig::get($artifact_name);
$config = $artifact_config['metadata'] ?? null;
if ($config === null) {
logger()->debug("No metadata for artifact: {$artifact_name}");
return false;
}
$license_type = $config['license'] ?? null;
$license_files = $config['license-files'] ?? [];
// Ensure license_files is array
if (is_string($license_files)) {
$license_files = [$license_files];
}
if (empty($license_files)) {
logger()->debug("No license files specified for: {$artifact_name}");
return false;
}
// Record in summary
$summary_license = $license_type ?? 'Custom';
if (!isset($license_summary[$summary_license])) {
$license_summary[$summary_license] = [];
}
$license_summary[$summary_license][] = $artifact_name;
// Dump each license file
$file_count = count($license_files);
$dumped_any = false;
foreach ($license_files as $index => $license_file_path) {
// Construct output filename
if ($file_count === 1) {
$output_filename = "{$artifact_name}_LICENSE.txt";
} else {
$output_filename = "{$artifact_name}_LICENSE_{$index}.txt";
}
$output_path = "{$target_dir}/{$output_filename}";
// Skip if file already exists (avoid duplicate writes)
if (file_exists($output_path)) {
logger()->debug("License file already exists, skipping: {$output_filename}");
$dumped_any = true; // Still count as dumped
continue;
}
// Try to read license file from source directory
$license_content = $this->readLicenseFile($artifact, $license_file_path);
if ($license_content === null) {
logger()->warning("License file not found for {$artifact_name}: {$license_file_path}");
continue;
}
// Write to target
if (file_put_contents($output_path, $license_content) === false) {
throw new SPCInternalException("Failed to write license file: {$output_path}");
}
logger()->info("Dumped license: {$output_filename}");
$dumped_any = true;
}
return $dumped_any;
}
/**
* Read license file content from artifact's source directory.
*
* @param Artifact $artifact Artifact instance
* @param string $license_file_path Relative path to license file
* @return null|string License content, or null if not found
*/
private function readLicenseFile(Artifact $artifact, string $license_file_path): ?string
{
$artifact_name = $artifact->getName();
// replace
if (str_starts_with($license_file_path, '@/')) {
$license_file_path = str_replace('@/', ROOT_DIR . '/src/globals/licenses/', $license_file_path);
}
$source_dir = $artifact->getSourceDir();
if (FileSystem::isRelativePath($license_file_path)) {
$full_path = "{$source_dir}/{$license_file_path}";
} else {
$full_path = $license_file_path;
}
// Try source directory first (if extracted)
if ($artifact->isSourceExtracted() || file_exists($full_path)) {
logger()->debug("Checking license file: {$full_path}");
if (file_exists($full_path)) {
logger()->info("Reading license from source: {$full_path}");
return file_get_contents($full_path);
}
} else {
logger()->warning("Artifact source not extracted: {$artifact_name}");
}
// Fallback: try SOURCE_PATH directly
$fallback_path = SOURCE_PATH . "/{$artifact_name}/{$license_file_path}";
logger()->debug("Checking fallback path: {$fallback_path}");
if (file_exists($fallback_path)) {
logger()->info("Reading license from fallback path: {$fallback_path}");
return file_get_contents($fallback_path);
}
logger()->debug("License file not found in any location for {$artifact_name}");
return null;
}
/**
* Generate SUMMARY.json file with read-modify-write support.
*
* @param string $target_dir Target directory
* @param array<string, array> $license_summary License summary data (license_type => [artifacts])
*/
private function generateSummary(string $target_dir, array $license_summary): void
{
if (empty($license_summary)) {
logger()->debug('No licenses to summarize');
return;
}
$summary_file = "{$target_dir}/SUMMARY.json";
// Read existing summary if exists
$existing_data = [];
if (file_exists($summary_file)) {
$content = file_get_contents($summary_file);
$existing_data = json_decode($content, true) ?? [];
logger()->debug('Loaded existing SUMMARY.json');
}
// Initialize structure
if (!isset($existing_data['artifacts'])) {
$existing_data['artifacts'] = [];
}
if (!isset($existing_data['summary'])) {
$existing_data['summary'] = ['license-types' => []];
}
// Merge new license information
foreach ($license_summary as $license_type => $artifacts) {
foreach ($artifacts as $artifact_name) {
// Add/update artifact info
$existing_data['artifacts'][$artifact_name] = [
'license' => $license_type,
'dumped-at' => date('Y-m-d H:i:s'),
];
// Update license_types summary
if (!isset($existing_data['summary']['license-types'][$license_type])) {
$existing_data['summary']['license-types'][$license_type] = [];
}
if (!in_array($artifact_name, $existing_data['summary']['license-types'][$license_type])) {
$existing_data['summary']['license-types'][$license_type][] = $artifact_name;
}
}
}
// Sort license types and artifacts
ksort($existing_data['summary']['license-types']);
foreach ($existing_data['summary']['license-types'] as &$artifacts) {
sort($artifacts);
}
ksort($existing_data['artifacts']);
// Update totals
$existing_data['summary']['total-artifacts'] = count($existing_data['artifacts']);
$existing_data['summary']['total-license-types'] = count($existing_data['summary']['license-types']);
$existing_data['summary']['last-updated'] = date('Y-m-d H:i:s');
// Write JSON file
file_put_contents(
$summary_file,
json_encode($existing_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
logger()->info('Generated SUMMARY.json with ' . $existing_data['summary']['total-artifacts'] . ' artifact(s)');
}
}