mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-03-19 21:34:53 +08:00
Add LicenseDumper component
This commit is contained in:
parent
ae748757d1
commit
7b725bb4da
147
src/StaticPHP/Command/DumpLicenseCommand.php
Normal file
147
src/StaticPHP/Command/DumpLicenseCommand.php
Normal file
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command;
|
||||
|
||||
use StaticPHP\Registry\PackageLoader;
|
||||
use StaticPHP\Util\DependencyResolver;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\LicenseDumper;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
#[AsCommand('dump-license', 'Dump licenses for artifacts')]
|
||||
class DumpLicenseCommand extends BaseCommand
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->addArgument('artifacts', InputArgument::OPTIONAL, 'Specific artifacts to dump licenses, comma separated, e.g "php-src,openssl,curl"');
|
||||
|
||||
// v2 compatible options
|
||||
$this->addOption('for-extensions', 'e', InputOption::VALUE_REQUIRED, 'Dump by extensions (automatically includes php-src), e.g "openssl,mbstring"');
|
||||
$this->addOption('for-libs', 'l', InputOption::VALUE_REQUIRED, 'Dump by libraries, e.g "openssl,zlib,curl"');
|
||||
|
||||
// v3 options
|
||||
$this->addOption('for-packages', 'p', InputOption::VALUE_REQUIRED, 'Dump by packages, e.g "php,libssl,libcurl"');
|
||||
$this->addOption('dump-dir', 'd', InputOption::VALUE_REQUIRED, 'Target directory for dumped licenses', BUILD_ROOT_PATH . '/license');
|
||||
$this->addOption('without-suggests', null, null, 'Do not include suggested packages when using --for-extensions or --for-packages');
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$dumper = new LicenseDumper();
|
||||
$dump_dir = $this->getOption('dump-dir');
|
||||
$artifacts_to_dump = [];
|
||||
|
||||
// Handle direct artifact argument
|
||||
if ($artifacts = $this->getArgument('artifacts')) {
|
||||
$artifacts_to_dump = array_merge($artifacts_to_dump, parse_comma_list($artifacts));
|
||||
}
|
||||
|
||||
// Handle --for-extensions option
|
||||
if ($exts = $this->getOption('for-extensions')) {
|
||||
$artifacts_to_dump = array_merge(
|
||||
$artifacts_to_dump,
|
||||
$this->resolveFromExtensions(parse_extension_list($exts))
|
||||
);
|
||||
}
|
||||
|
||||
// Handle --for-libs option (v2 compat)
|
||||
if ($libs = $this->getOption('for-libs')) {
|
||||
$artifacts_to_dump = array_merge(
|
||||
$artifacts_to_dump,
|
||||
$this->resolveFromPackages(parse_comma_list($libs))
|
||||
);
|
||||
}
|
||||
|
||||
// Handle --for-packages option
|
||||
if ($packages = $this->getOption('for-packages')) {
|
||||
$artifacts_to_dump = array_merge(
|
||||
$artifacts_to_dump,
|
||||
$this->resolveFromPackages(parse_comma_list($packages))
|
||||
);
|
||||
}
|
||||
|
||||
// Check if any artifacts to dump
|
||||
if (empty($artifacts_to_dump)) {
|
||||
$this->output->writeln('<error>No artifacts specified. Use one of:</error>');
|
||||
$this->output->writeln(' - Direct argument: <info>dump-license php-src,openssl,curl</info>');
|
||||
$this->output->writeln(' - --for-extensions: <info>dump-license --for-extensions=openssl,mbstring</info>');
|
||||
$this->output->writeln(' - --for-libs: <info>dump-license --for-libs=openssl,zlib</info>');
|
||||
$this->output->writeln(' - --for-packages: <info>dump-license --for-packages=php,libssl</info>');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// Deduplicate artifacts
|
||||
$artifacts_to_dump = array_values(array_unique($artifacts_to_dump));
|
||||
|
||||
logger()->info('Dumping licenses for ' . count($artifacts_to_dump) . ' artifact(s)');
|
||||
logger()->debug('Artifacts: ' . implode(', ', $artifacts_to_dump));
|
||||
|
||||
// Add artifacts to dumper
|
||||
$dumper->addArtifacts($artifacts_to_dump);
|
||||
|
||||
// Dump
|
||||
$success = $dumper->dump($dump_dir);
|
||||
|
||||
if ($success) {
|
||||
InteractiveTerm::success('Licenses dumped successfully: ' . $dump_dir);
|
||||
// $this->output->writeln("<info>✓ Successfully dumped licenses to: {$dump_dir}</info>");
|
||||
// $this->output->writeln("<comment> Total artifacts: " . count($artifacts_to_dump) . '</comment>');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->output->writeln('<error>Failed to dump licenses</error>');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve artifacts from extension names.
|
||||
*
|
||||
* @param array<string> $extensions Extension names
|
||||
* @return array<string> Artifact names
|
||||
*/
|
||||
private function resolveFromExtensions(array $extensions): array
|
||||
{
|
||||
// Convert extension names to package names
|
||||
$packages = array_map(fn ($ext) => "ext-{$ext}", $extensions);
|
||||
|
||||
// Automatically include php-related artifacts
|
||||
array_unshift($packages, 'php');
|
||||
array_unshift($packages, 'php-micro');
|
||||
array_unshift($packages, 'php-embed');
|
||||
array_unshift($packages, 'php-fpm');
|
||||
|
||||
return $this->resolveFromPackages($packages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve artifacts from package names.
|
||||
*
|
||||
* @param array<string> $packages Package names
|
||||
* @return array<string> Artifact names
|
||||
*/
|
||||
private function resolveFromPackages(array $packages): array
|
||||
{
|
||||
$artifacts = [];
|
||||
$include_suggests = !$this->getOption('without-suggests');
|
||||
|
||||
// Resolve package dependencies
|
||||
$resolved_packages = DependencyResolver::resolve($packages, [], $include_suggests);
|
||||
|
||||
foreach ($resolved_packages as $pkg_name) {
|
||||
try {
|
||||
$pkg = PackageLoader::getPackage($pkg_name);
|
||||
if ($artifact = $pkg->getArtifact()) {
|
||||
$artifacts[] = $artifact->getName();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
logger()->debug("Package {$pkg_name} has no artifact or failed to load: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($artifacts);
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ use StaticPHP\Command\Dev\ShellCommand;
|
||||
use StaticPHP\Command\Dev\SortConfigCommand;
|
||||
use StaticPHP\Command\DoctorCommand;
|
||||
use StaticPHP\Command\DownloadCommand;
|
||||
use StaticPHP\Command\DumpLicenseCommand;
|
||||
use StaticPHP\Command\ExtractCommand;
|
||||
use StaticPHP\Command\InstallPackageCommand;
|
||||
use StaticPHP\Command\SPCConfigCommand;
|
||||
@ -55,6 +56,7 @@ class ConsoleApplication extends Application
|
||||
new BuildLibsCommand(),
|
||||
new ExtractCommand(),
|
||||
new SPCConfigCommand(),
|
||||
new DumpLicenseCommand(),
|
||||
|
||||
// dev commands
|
||||
new ShellCommand(),
|
||||
|
||||
262
src/StaticPHP/Util/LicenseDumper.php
Normal file
262
src/StaticPHP/Util/LicenseDumper.php
Normal file
@ -0,0 +1,262 @@
|
||||
<?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 LICENSE-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<string, 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();
|
||||
|
||||
// Try source directory first (if extracted)
|
||||
if ($artifact->isSourceExtracted()) {
|
||||
$source_dir = $artifact->getSourceDir();
|
||||
$full_path = "{$source_dir}/{$license_file_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()->debug("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 LICENSE-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}/LICENSE-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 LICENSE-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 LICENSE-SUMMARY.json with ' . $existing_data['summary']['total_artifacts'] . ' artifact(s)');
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user