Artifact names to dump */ private array $artifacts = []; /** * Add artifacts by name. * * @param array $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 $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)'); } }