diff --git a/phpstan.neon b/phpstan.neon
index cf6e4974..45e512ba 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,6 +1,7 @@
parameters:
reportUnmatchedIgnoredErrors: false
level: 4
+ phpVersion: 80400
paths:
- ./src/
ignoreErrors:
diff --git a/skeleton-test.php b/skeleton-test.php
index 689d3385..59fbbb7e 100644
--- a/skeleton-test.php
+++ b/skeleton-test.php
@@ -1,8 +1,11 @@
addDependency('bar')
->addStaticLib('libfoo.a', 'unix')
->addStaticLib('libfoo.a', 'unix')
- ->addArtifact($artifact_generator = new ArtifactGenerator('foo')->setSource(['type' => 'url', 'url' => 'https://example.com/foo.tar.gz']));
+ ->addArtifact($artifact_generator = new ArtifactGenerator('foo')->setSource(['type' => 'url', 'url' => 'https://example.com/foo.tar.gz']))
+ ->enableBuild(['Darwin', 'Linux'], 'build')
+ ->addFunctionExecutorBinding('build', new ExecutorGenerator(UnixCMakeExecutor::class));
-$pkg_config = $package_generator->generateConfig();
-$artifact_config = $artifact_generator->generateConfig();
+$pkg_config = $package_generator->generateConfigArray();
+$artifact_config = $artifact_generator->generateConfigArray();
-echo "===== pkg.json =====" . PHP_EOL;
-echo json_encode($pkg_config, 64|128|256) . PHP_EOL;
-echo "===== artifact.json =====" . PHP_EOL;
-echo json_encode($artifact_config, 64|128|256) . PHP_EOL;
+echo '===== pkg.json =====' . PHP_EOL;
+echo json_encode($pkg_config, 64 | 128 | 256) . PHP_EOL;
+echo '===== artifact.json =====' . PHP_EOL;
+echo json_encode($artifact_config, 64 | 128 | 256) . PHP_EOL;
+echo '===== php code for package =====' . PHP_EOL;
+echo $package_generator->generatePackageClassFile('Package\Library');
diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php
index a7347395..2ac7b454 100644
--- a/src/Package/Artifact/zig.php
+++ b/src/Package/Artifact/zig.php
@@ -8,7 +8,6 @@ use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
use StaticPHP\Attribute\Artifact\CustomBinary;
-use StaticPHP\Attribute\Artifact\CustomSource;
use StaticPHP\Exception\DownloaderException;
use StaticPHP\Runtime\SystemTarget;
diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php
index 315cfb11..b53ddd8a 100644
--- a/src/StaticPHP/Artifact/ArtifactDownloader.php
+++ b/src/StaticPHP/Artifact/ArtifactDownloader.php
@@ -329,8 +329,7 @@ class ArtifactDownloader
}
if ($interactive) {
$skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : '';
- InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}", true);
- echo PHP_EOL;
+ InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}\n", true);
}
}
} catch (SPCException $e) {
diff --git a/src/StaticPHP/Command/BaseCommand.php b/src/StaticPHP/Command/BaseCommand.php
index e416be26..02f84ffb 100644
--- a/src/StaticPHP/Command/BaseCommand.php
+++ b/src/StaticPHP/Command/BaseCommand.php
@@ -23,7 +23,6 @@ abstract class BaseCommand extends Command
\___ \| __/ _` | __| |/ __| |_) | |_| | |_) |
___) | || (_| | |_| | (__| __/| _ | __/
|____/ \__\__,_|\__|_|\___|_| |_| |_|_| {version}
-
';
protected bool $no_motd = false;
@@ -71,7 +70,7 @@ abstract class BaseCommand extends Command
$version = $this->getVersionWithCommit();
if (!$this->no_motd) {
$str = str_replace('{version}', '' . ConsoleColor::none("v{$version}"), '' . ConsoleColor::magenta(self::$motd));
- echo $this->input->getOption('no-ansi') ? strip_ansi_colors($str) : $str;
+ $this->output->writeln($this->input->getOption('no-ansi') ? strip_ansi_colors($str) : $str);
}
}
diff --git a/src/StaticPHP/Command/Dev/SkeletonCommand.php b/src/StaticPHP/Command/Dev/SkeletonCommand.php
new file mode 100644
index 00000000..b19eb4a1
--- /dev/null
+++ b/src/StaticPHP/Command/Dev/SkeletonCommand.php
@@ -0,0 +1,402 @@
+output->writeln('The dev:skel command is not available in phar mode.');
+ return 1;
+ }
+ if (SystemTarget::getTargetOS() === 'Windows') {
+ $this->output->writeln('The dev:skel command is not available on Windows systems.');
+ return 1;
+ }
+
+ $this->runMainMenu();
+
+ return 0;
+ }
+
+ public function validatePackageName(string $name): ?string
+ {
+ if (!preg_match('/^[a-zA-Z0-9_-]+$/', $name)) {
+ return 'Library name can only contain letters, numbers, underscores, and hyphens.';
+ }
+ // must start with a letter
+ if (!preg_match('/^[a-zA-Z]/', $name)) {
+ return 'Library name must start with a letter.';
+ }
+ return null;
+ }
+
+ private function runMainMenu(): void
+ {
+ $main = select('Please select the skeleton option', [
+ 'library' => 'Create a new library package',
+ 'target' => 'Create a new target package',
+ 'php-extension' => 'Create a new PHP extension',
+ 'q' => 'Exit',
+ ]);
+ $generator = match ($main) {
+ 'library' => $this->runCreateLib(),
+ 'target' => $this->runCreateTarget(),
+ 'php-extension' => $this->runCreateExt(),
+ 'q' => exit(0),
+ default => null,
+ };
+ $write = $generator->writeAll();
+ $this->output->writeln("Package config in: {$write['package_config']}");
+ $this->output->writeln("Artifact config in: {$write['artifact_config']}");
+ $this->output->writeln('Package class:');
+ $this->output->writeln($write['package_class_content']);
+ }
+
+ private function runCreateLib(): PackageGenerator
+ {
+ // init empty
+ $static_libs = '';
+ $headers = '';
+ $static_bins = '';
+ $pkg_configs = '';
+
+ // ask name
+ $package_name = text('Please enter your library name', placeholder: 'e.g. pcre2', validate: [$this, 'validatePackageName']);
+
+ // ask OS
+ $os = select("[{$package_name}] On which OS family do you want to build this library?", [
+ 'unix' => 'Both Linux and Darwin (unix-like OS)',
+ 'linux' => 'Linux only',
+ 'macos' => 'Darwin(macOS) only',
+ 'windows' => 'Windows only',
+ 'all' => 'All supported OS (' . implode(', ', SUPPORTED_OS_FAMILY) . ')',
+ ]);
+
+ $produce = select("[{$package_name}] What does this library produce?", [
+ 'static_libs' => 'Static Libraries (.a/.lib)',
+ 'headers' => 'Header Files (.h)',
+ 'static_bins' => 'Static Binaries (executables)',
+ 'pkg_configs' => 'Pkg-Config files (.pc)',
+ 'all' => 'All of the above',
+ ]);
+
+ if ($produce === 'all' || $produce === 'static_libs') {
+ $static_libs = text(
+ 'Please enter the names of the static libraries produced',
+ placeholder: 'e.g. libpcre2.a, libbar.a',
+ default: str_starts_with($package_name, 'lib') ? "{$package_name}.a" : "lib{$package_name}.a",
+ validate: function ($value) {
+ $names = array_map('trim', explode(',', $value));
+ if (array_any($names, fn ($name) => !preg_match('/^[a-zA-Z0-9_.-]+$/', $name))) {
+ return 'Library names can only contain letters, numbers, underscores, hyphens, and dots.';
+ }
+ return null;
+ },
+ hint: 'Separate multiple names with commas'
+ );
+ }
+ if ($produce === 'all' || $produce === 'headers') {
+ $headers = text(
+ 'Please enter the names of the header files produced',
+ placeholder: 'e.g. foo.h, bar.h',
+ default: str_starts_with($package_name, 'lib') ? str_replace('lib', '', $package_name) . '.h' : $package_name . '.h',
+ validate: function ($value) {
+ $names = array_map('trim', explode(',', $value));
+ if (array_any($names, fn ($name) => !preg_match('/^[a-zA-Z0-9_.-]+$/', $name))) {
+ return 'Header file names can only contain letters, numbers, underscores, hyphens, and dots.';
+ }
+ return null;
+ },
+ hint: 'Separate multiple names with commas, directories are allowed (e.g. openssl directory)'
+ );
+ }
+ if ($produce === 'all' || $produce === 'static_bins') {
+ $static_bins = text(
+ 'Please enter the names of the static binaries produced',
+ placeholder: 'e.g. foo, bar',
+ default: $package_name,
+ validate: function ($value) {
+ $names = array_map('trim', explode(',', $value));
+ if (array_any($names, fn ($name) => !preg_match('/^[a-zA-Z0-9_.-]+$/', $name))) {
+ return 'Binary names can only contain letters, numbers, underscores, hyphens, and dots.';
+ }
+ return null;
+ },
+ hint: 'Separate multiple names with commas'
+ );
+ }
+ if ($produce === 'all' || $produce === 'pkg_configs') {
+ $pkg_configs = text(
+ 'Please enter the names of the pkg-config files produced',
+ placeholder: 'e.g. foo.pc, bar.pc',
+ default: str_starts_with($package_name, 'lib') ? str_replace('lib', '', $package_name) . '.pc' : $package_name . '.pc',
+ validate: function ($value) {
+ if (!str_ends_with($value, '.pc')) {
+ return 'Pkg-config file names must end with .pc extension.';
+ }
+ return null;
+ },
+ hint: 'Separate multiple names with commas'
+ );
+ }
+
+ if ($headers === '' && $static_bins === '' && $static_libs === '' && $pkg_configs === '') {
+ $this->output->writeln('You must specify at least one of static libraries, header files, or static binaries produced.');
+ exit(1);
+ }
+
+ // ask source
+ $artifact_generator = $this->runCreateArtifact($package_name, true, false, null);
+ $package_generator = new PackageGenerator($package_name, 'library');
+ // set artifact
+ $package_generator = $package_generator->addArtifact($artifact_generator);
+ // set os
+ $package_generator = match ($os) {
+ 'unix' => $package_generator->enableBuild(['Darwin', 'Linux'], 'build'),
+ 'linux' => $package_generator->enableBuild(['Linux'], 'build'),
+ 'macos' => $package_generator->enableBuild(['Darwin'], 'build'),
+ 'windows' => $package_generator->enableBuild(['Windows'], 'build'),
+ 'all' => $package_generator->enableBuild(SUPPORTED_OS_FAMILY, 'build'),
+ default => $package_generator,
+ };
+ // set produce
+ if ($static_libs !== '') {
+ $lib_names = array_map('trim', explode(',', $static_libs));
+ foreach ($lib_names as $lib_name) {
+ $package_generator = $package_generator->addStaticLib($lib_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os));
+ }
+ }
+ if ($headers !== '') {
+ $header_names = array_map('trim', explode(',', $headers));
+ foreach ($header_names as $header_name) {
+ $package_generator = $package_generator->addHeaderFile($header_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os));
+ }
+ }
+ if ($static_bins !== '') {
+ $bin_names = array_map('trim', explode(',', $static_bins));
+ foreach ($bin_names as $bin_name) {
+ $package_generator = $package_generator->addStaticBin($bin_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os));
+ }
+ }
+ if ($pkg_configs !== '') {
+ $pc_names = array_map('trim', explode(',', $pkg_configs));
+ foreach ($pc_names as $pc_name) {
+ $package_generator = $package_generator->addPkgConfigFile($pc_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os));
+ }
+ }
+ // ask for package config writing selection, same as artifact
+ $package_configs = Registry::getLoadedPackageConfigs();
+ $package_config_file = select("[{$package_name}] Please select the package config file to write the package config to", $package_configs);
+ return $package_generator->setConfigFile($package_config_file);
+ }
+
+ private function runCreateArtifact(
+ string $package_name,
+ ?bool $create_source,
+ ?bool $create_binary,
+ string|true|null $default_extract_dir = true
+ ): ArtifactGenerator {
+ $artifact = new ArtifactGenerator($package_name);
+
+ if ($create_source === null) {
+ $create_source = confirm("[{$package_name}] Do you want to create a source artifact?");
+ }
+
+ if (!$create_source) {
+ goto binary;
+ }
+
+ $source_type = select("[{$package_name}] Where is the source code located?", SPC_DOWNLOAD_TYPE_DISPLAY_NAME);
+
+ $source_config = $this->askDownloadTypeConfig($package_name, $source_type, $default_extract_dir, 'source');
+ $artifact = $artifact->setSource($source_config);
+
+ binary:
+ if ($create_binary === null) {
+ $create_binary = confirm("[{$package_name}] Do you want to create a binary artifact?");
+ }
+
+ if (!$create_binary) {
+ goto end;
+ }
+
+ $binary_fix = [
+ 'macos-x86_64' => null,
+ 'macos-aarch64' => null,
+ 'linux-x86_64' => null,
+ 'linux-aarch64' => null,
+ 'windows-x86_64' => null,
+ ];
+ while (($os = select("[{$package_name}] Please configure the binary downloading options for OS", [
+ 'macos-x86_64' => 'macos-x86_64' . ($binary_fix['macos-x86_64'] ? ' (done)' : ''),
+ 'macos-aarch64' => 'macos-aarch64' . ($binary_fix['macos-aarch64'] ? ' (done)' : ''),
+ 'linux-x86_64' => 'linux-x86_64' . ($binary_fix['linux-x86_64'] ? ' (done)' : ''),
+ 'linux-aarch64' => 'linux-aarch64' . ($binary_fix['linux-aarch64'] ? ' (done)' : ''),
+ 'windows-x86_64' => 'windows-x86_64' . ($binary_fix['windows-x86_64'] ? ' (done)' : ''),
+ 'copy' => 'Duplicate from another OS',
+ 'finish' => 'Submit',
+ ])) !== 'finish') {
+ $source_type = select("[{$package_name}] Where is the binary for {$os} located?", SPC_DOWNLOAD_TYPE_DISPLAY_NAME);
+ $source_config = $this->askDownloadTypeConfig($package_name, $source_type, $default_extract_dir, 'binary');
+ // set to artifact
+ $artifact = $artifact->setBinary($os, $source_config);
+ $binary_fix[$os] = true;
+ }
+
+ end:
+
+ // generate config files, select existing package config file to write
+ $artifact_configs = Registry::getLoadedArtifactConfigs();
+ $artifact_config_file = select("[{$package_name}] Please select the artifact config file to write the artifact config to", $artifact_configs);
+ return $artifact->setConfigFile($artifact_config_file);
+ }
+
+ private function runCreateTarget(): PackageGenerator
+ {
+ throw new WrongUsageException('Not implemented');
+ }
+
+ private function runCreateExt(): PackageGenerator
+ {
+ throw new WrongUsageException('Not implemented');
+ }
+
+ private function askDownloadTypeConfig(string $package_name, int|string $source_type, bool|string|null $default_extract_dir, string $config_type): array
+ {
+ $source_config = ['type' => $source_type];
+ switch ($source_type) {
+ case 'bitbuckettag':
+ $source_config['repo'] = text("[{$package_name}] Please enter the BitBucket repository (e.g. user/repo)");
+ break;
+ case 'filelist':
+ $source_config['url'] = text(
+ "[{$package_name}] Please enter the file index website URL",
+ placeholder: 'e.g. https://ftp.gnu.org/pub/gnu/gettext/',
+ hint: 'Make sure the target url is a directory listing page like ftp.gnu.org.'
+ );
+ $source_config['regex'] = text(
+ "[{$package_name}] Please enter the regex pattern to match the archive file",
+ placeholder: 'e.g. /gettext-(\d+\.\d+(\.\d+)?)\.tar\.gz/',
+ default: "/href=\"(?{$package_name}-(?[^\"]+)\\.tar\\.gz)\"/",
+ hint: 'Make sure the regex contains a capturing group for the version number.'
+ );
+ break;
+ case 'git':
+ $source_config['url'] = text(
+ "[{$package_name}] Please enter the Git repository URL",
+ validate: function ($value) {
+ if (!filter_var($value, FILTER_VALIDATE_URL) && !preg_match('/^(git|ssh|http|https|git@[-\w.]+):(\/\/)?(.*?)(\.git)(\/?|#[-\d\w._]+?)$/', $value)) {
+ return 'Please enter a valid Git repository URL.';
+ }
+ return null;
+ },
+ hint: 'e.g. https://github.com/user/repo.git'
+ );
+ $source_config['rev'] = text(
+ "[{$package_name}] Please enter the Git revision (branch, tag, or commit hash)",
+ default: 'main',
+ hint: 'e.g. main, master, v1.0.0, or a commit hash'
+ );
+ break;
+ case 'ghrel':
+ $source_config['repo'] = text("[{$package_name}] Please enter the GitHub repository (e.g. user/repo)");
+ $source_config['match'] = text(
+ "[{$package_name}] Please enter the regex pattern to match the source archive file",
+ placeholder: 'e.g. /foo-(\d+\.\d+(\.\d+)?)\.tar\.gz/',
+ default: "{$package_name}-.+\\.tar\\.gz",
+ );
+ break;
+ case 'ghtar':
+ case 'ghtagtar':
+ $source_config['repo'] = text("[{$package_name}] Please enter the GitHub repository (e.g. user/repo)");
+ $source_config['prefer-stable'] = confirm("[{$package_name}] Do you want to prefer stable releases?");
+ if ($source_type === 'ghtagtar' && confirm('Do you want to match tags with a specific pattern?', default: false)) {
+ $source_config['match'] = text(
+ "[{$package_name}] Please enter the regex pattern to match tags",
+ placeholder: 'e.g. v(\d+\.\d+(\.\d+)?)',
+ );
+ }
+ break;
+ case 'local':
+ $source_config['dirname'] = text(
+ "[{$package_name}] Please enter the local directory path",
+ validate: function ($value) {
+ if (trim($value) === '') {
+ return 'Local source directory cannot be empty.';
+ }
+ if (!is_dir($value)) {
+ return 'The specified local source directory does not exist.';
+ }
+ return null;
+ },
+ );
+ break;
+ case 'pie':
+ $source_config['repo'] = text(
+ "[{$package_name}] Please enter the PIE repository name",
+ placeholder: 'e.g. user/repo',
+ );
+ break;
+ case 'url':
+ $source_config['url'] = text(
+ "[{$package_name}] Please enter the file download URL",
+ validate: function ($value) {
+ if (!filter_var($value, FILTER_VALIDATE_URL)) {
+ return 'Please enter a valid URL.';
+ }
+ return null;
+ },
+ );
+ break;
+ case 'custom':
+ break;
+ }
+ // ask extract dir if is true
+ if ($default_extract_dir === true) {
+ if (confirm('Do you want to specify a custom extract directory?')) {
+ $extract_hint = match ($config_type) {
+ 'source' => 'the source will be from the `source/` dir by default',
+ 'binary' => 'the binary will be from the `pkgroot/{arch}-{os}/` dir by default',
+ default => '',
+ };
+ $default_extract_dir = text(
+ "[{$package_name}] Please enter the source extract directory",
+ validate: function ($value) {
+ if (trim($value) === '') {
+ return 'Extract directory cannot be empty.';
+ }
+ return null;
+ },
+ hint: 'You can use relative path, ' . $extract_hint . '.'
+ );
+ } else {
+ $default_extract_dir = null;
+ }
+ }
+ if ($default_extract_dir !== null) {
+ $source_config['extract'] = $default_extract_dir;
+ }
+
+ // return config
+ return $source_config;
+ }
+}
diff --git a/src/StaticPHP/Command/Dev/SortConfigCommand.php b/src/StaticPHP/Command/Dev/SortConfigCommand.php
new file mode 100644
index 00000000..aa3a9ecd
--- /dev/null
+++ b/src/StaticPHP/Command/Dev/SortConfigCommand.php
@@ -0,0 +1,49 @@
+sortConfigFile($file);
+ }
+ $loaded_pkg_configs = Registry::getLoadedPackageConfigs();
+ foreach ($loaded_pkg_configs as $file) {
+ $this->sortConfigFile($file);
+ }
+ return static::SUCCESS;
+ }
+
+ private function sortConfigFile(mixed $file): void
+ {
+ $content = file_get_contents($file);
+ if ($content === false) {
+ $this->output->writeln("Failed to read artifact config file: {$file}");
+ return;
+ }
+ $data = json_decode($content, true);
+ if (!is_array($data)) {
+ $this->output->writeln("Invalid JSON format in artifact config file: {$file}");
+ return;
+ }
+ ksort($data);
+ foreach ($data as $artifact_name => &$config) {
+ ksort($config);
+ }
+ unset($config);
+ $new_content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
+ file_put_contents($file, $new_content);
+ $this->output->writeln("Sorted artifact config file: {$file}");
+ }
+}
diff --git a/src/StaticPHP/Config/ArtifactConfig.php b/src/StaticPHP/Config/ArtifactConfig.php
index e840c0bf..49abae92 100644
--- a/src/StaticPHP/Config/ArtifactConfig.php
+++ b/src/StaticPHP/Config/ArtifactConfig.php
@@ -11,26 +11,30 @@ class ArtifactConfig
{
private static array $artifact_configs = [];
- public static function loadFromDir(string $dir, string $registry_name): void
+ public static function loadFromDir(string $dir, string $registry_name): array
{
if (!is_dir($dir)) {
throw new WrongUsageException("Directory {$dir} does not exist, cannot load artifact config.");
}
+ $loaded = [];
$files = glob("{$dir}/artifact.*.json");
if (is_array($files)) {
foreach ($files as $file) {
self::loadFromFile($file, $registry_name);
+ $loaded[] = $file;
}
}
if (file_exists("{$dir}/artifact.json")) {
self::loadFromFile("{$dir}/artifact.json", $registry_name);
+ $loaded[] = "{$dir}/artifact.json";
}
+ return $loaded;
}
/**
* Load artifact configurations from a specified JSON file.
*/
- public static function loadFromFile(string $file, string $registry_name): void
+ public static function loadFromFile(string $file, string $registry_name): string
{
$content = file_get_contents($file);
if ($content === false) {
@@ -45,6 +49,7 @@ class ArtifactConfig
self::$artifact_configs[$artifact_name] = $config;
Registry::_bindArtifactConfigFile($artifact_name, $registry_name, $file);
}
+ return $file;
}
/**
diff --git a/src/StaticPHP/Config/PackageConfig.php b/src/StaticPHP/Config/PackageConfig.php
index 3342dfc7..56ef7ab1 100644
--- a/src/StaticPHP/Config/PackageConfig.php
+++ b/src/StaticPHP/Config/PackageConfig.php
@@ -16,20 +16,24 @@ class PackageConfig
* Load package configurations from a specified directory.
* It will look for files matching the pattern 'pkg.*.json' and 'pkg.json'.
*/
- public static function loadFromDir(string $dir, string $registry_name): void
+ public static function loadFromDir(string $dir, string $registry_name): array
{
if (!is_dir($dir)) {
throw new WrongUsageException("Directory {$dir} does not exist, cannot load pkg.json config.");
}
+ $loaded = [];
$files = glob("{$dir}/pkg.*.json");
if (is_array($files)) {
foreach ($files as $file) {
self::loadFromFile($file, $registry_name);
+ $loaded[] = $file;
}
}
if (file_exists("{$dir}/pkg.json")) {
self::loadFromFile("{$dir}/pkg.json", $registry_name);
+ $loaded[] = "{$dir}/pkg.json";
}
+ return $loaded;
}
/**
@@ -37,7 +41,7 @@ class PackageConfig
*
* @param string $file the path to the json package configuration file
*/
- public static function loadFromFile(string $file, string $registry_name): void
+ public static function loadFromFile(string $file, string $registry_name): string
{
$content = file_get_contents($file);
if ($content === false) {
@@ -52,6 +56,7 @@ class PackageConfig
self::$package_configs[$pkg_name] = $config;
Registry::_bindPackageConfigFile($pkg_name, $registry_name, $file);
}
+ return $file;
}
/**
diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php
index bd0cfd60..fd7650aa 100644
--- a/src/StaticPHP/ConsoleApplication.php
+++ b/src/StaticPHP/ConsoleApplication.php
@@ -10,6 +10,7 @@ use StaticPHP\Command\Dev\EnvCommand;
use StaticPHP\Command\Dev\IsInstalledCommand;
use StaticPHP\Command\Dev\ShellCommand;
use StaticPHP\Command\Dev\SkeletonCommand;
+use StaticPHP\Command\Dev\SortConfigCommand;
use StaticPHP\Command\DoctorCommand;
use StaticPHP\Command\DownloadCommand;
use StaticPHP\Command\ExtractCommand;
@@ -61,6 +62,7 @@ class ConsoleApplication extends Application
new IsInstalledCommand(),
new EnvCommand(),
new SkeletonCommand(),
+ new SortConfigCommand(),
]);
// add additional commands from registries
diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php
index 4c44ce92..ae3b7346 100644
--- a/src/StaticPHP/Package/PackageInstaller.php
+++ b/src/StaticPHP/Package/PackageInstaller.php
@@ -133,9 +133,8 @@ class PackageInstaller
// show install or build options in terminal with beautiful output
$this->printInstallerInfo();
- InteractiveTerm::notice('Build process will start after 2s ...');
+ InteractiveTerm::notice('Build process will start after 2s ...' . PHP_EOL);
sleep(2);
- echo PHP_EOL;
}
// Early validation: check if packages can be built or installed before downloading
diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php
index 909de021..e464ed47 100644
--- a/src/StaticPHP/Registry/Registry.php
+++ b/src/StaticPHP/Registry/Registry.php
@@ -13,13 +13,39 @@ use Symfony\Component\Yaml\Yaml;
class Registry
{
- /** @var array List of loaded registries */
+ /** @var string[] List of loaded registries */
private static array $loaded_registries = [];
+ /** @var array Loaded registry configs */
+ private static array $registry_configs = [];
+
+ private static array $loaded_package_configs = [];
+
+ private static array $loaded_artifact_configs = [];
+
/** @var array Maps of package and artifact names to their registry config file paths (for reverse lookup) */
private static array $package_reversed_registry_files = [];
+
private static array $artifact_reversed_registry_files = [];
+ /**
+ * Get the current registry configuration.
+ * "Current" depends on SPC load mode
+ */
+ public static function getRegistryConfig(?string $registry_name = null): array
+ {
+ if ($registry_name === null && spc_mode(SPC_MODE_SOURCE)) {
+ return self::$registry_configs['internal'];
+ }
+ if ($registry_name !== null && isset(self::$registry_configs[$registry_name])) {
+ return self::$registry_configs[$registry_name];
+ }
+ if ($registry_name === null) {
+ throw new RegistryException('No registry name specified.');
+ }
+ throw new RegistryException("Registry '{$registry_name}' is not loaded.");
+ }
+
/**
* Load a registry from file path.
* This method handles external registries that may not be in composer autoload.
@@ -52,12 +78,14 @@ class Registry
return;
}
self::$loaded_registries[] = $registry_name;
+ self::$registry_configs[$registry_name] = $data;
+ self::$registry_configs[$registry_name]['_file'] = $registry_file;
logger()->debug("Loading registry '{$registry_name}' from file: {$registry_file}");
// Load composer autoload if specified (for external registries with their own dependencies)
if (isset($data['autoload']) && is_string($data['autoload'])) {
- $autoload_path = self::fullpath($data['autoload'], dirname($registry_file));
+ $autoload_path = FileSystem::fullpath($data['autoload'], dirname($registry_file));
if (file_exists($autoload_path)) {
logger()->debug("Loading external autoload from: {$autoload_path}");
require_once $autoload_path;
@@ -69,7 +97,7 @@ class Registry
// load doctor items from PSR-4 directories
if (isset($data['doctor']['psr-4']) && is_assoc_array($data['doctor']['psr-4'])) {
foreach ($data['doctor']['psr-4'] as $namespace => $path) {
- $path = self::fullpath($path, dirname($registry_file));
+ $path = FileSystem::fullpath($path, dirname($registry_file));
DoctorLoader::loadFromPsr4Dir($path, $namespace, $auto_require);
}
}
@@ -87,11 +115,11 @@ class Registry
// load package configs
if (isset($data['package']['config']) && is_array($data['package']['config'])) {
foreach ($data['package']['config'] as $path) {
- $path = self::fullpath($path, dirname($registry_file));
+ $path = FileSystem::fullpath($path, dirname($registry_file));
if (is_file($path)) {
- PackageConfig::loadFromFile($path, $registry_name);
+ self::$loaded_package_configs[] = PackageConfig::loadFromFile($path, $registry_name);
} elseif (is_dir($path)) {
- PackageConfig::loadFromDir($path, $registry_name);
+ self::$loaded_package_configs = array_merge(self::$loaded_package_configs, PackageConfig::loadFromDir($path, $registry_name));
}
}
}
@@ -99,11 +127,11 @@ class Registry
// load artifact configs
if (isset($data['artifact']['config']) && is_array($data['artifact']['config'])) {
foreach ($data['artifact']['config'] as $path) {
- $path = self::fullpath($path, dirname($registry_file));
+ $path = FileSystem::fullpath($path, dirname($registry_file));
if (is_file($path)) {
- ArtifactConfig::loadFromFile($path, $registry_name);
+ self::$loaded_artifact_configs[] = ArtifactConfig::loadFromFile($path, $registry_name);
} elseif (is_dir($path)) {
- ArtifactConfig::loadFromDir($path, $registry_name);
+ self::$loaded_package_configs = array_merge(self::$loaded_package_configs, ArtifactConfig::loadFromDir($path, $registry_name));
}
}
}
@@ -111,7 +139,7 @@ class Registry
// load packages from PSR-4 directories
if (isset($data['package']['psr-4']) && is_assoc_array($data['package']['psr-4'])) {
foreach ($data['package']['psr-4'] as $namespace => $path) {
- $path = self::fullpath($path, dirname($registry_file));
+ $path = FileSystem::fullpath($path, dirname($registry_file));
PackageLoader::loadFromPsr4Dir($path, $namespace, $auto_require);
}
}
@@ -129,7 +157,7 @@ class Registry
// load artifacts from PSR-4 directories
if (isset($data['artifact']['psr-4']) && is_assoc_array($data['artifact']['psr-4'])) {
foreach ($data['artifact']['psr-4'] as $namespace => $path) {
- $path = self::fullpath($path, dirname($registry_file));
+ $path = FileSystem::fullpath($path, dirname($registry_file));
ArtifactLoader::loadFromPsr4Dir($path, $namespace, $auto_require);
}
}
@@ -147,7 +175,7 @@ class Registry
// load additional commands from PSR-4 directories
if (isset($data['command']['psr-4']) && is_assoc_array($data['command']['psr-4'])) {
foreach ($data['command']['psr-4'] as $namespace => $path) {
- $path = self::fullpath($path, dirname($registry_file));
+ $path = FileSystem::fullpath($path, dirname($registry_file));
$classes = FileSystem::getClassesPsr4($path, $namespace, auto_require: $auto_require);
$instances = array_map(fn ($x) => new $x(), $classes);
ConsoleApplication::_addAdditionalCommands($instances);
@@ -262,6 +290,16 @@ class Registry
return self::$artifact_reversed_registry_files[$artifact_name] ?? null;
}
+ public static function getLoadedPackageConfigs(): array
+ {
+ return self::$loaded_package_configs;
+ }
+
+ public static function getLoadedArtifactConfigs(): array
+ {
+ return self::$loaded_artifact_configs;
+ }
+
/**
* Parse a class entry from the classes array.
* Supports two formats:
@@ -298,7 +336,7 @@ class Registry
// If file path is provided, require it
if ($file_path !== null) {
- $full_path = self::fullpath($file_path, $base_path);
+ $full_path = FileSystem::fullpath($file_path, $base_path);
require_once $full_path;
return;
}
@@ -311,21 +349,4 @@ class Registry
" 3. Provide file path in classes map: \"{$class}\": \"path/to/file.php\""
);
}
-
- /**
- * Return full path, resolving relative paths against a base path.
- *
- * @param string $path Input path (relative or absolute)
- * @param string $relative_path_base Base path for relative paths
- */
- private static function fullpath(string $path, string $relative_path_base): string
- {
- if (FileSystem::isRelativePath($path)) {
- $path = $relative_path_base . DIRECTORY_SEPARATOR . $path;
- }
- if (!file_exists($path)) {
- throw new RegistryException("Path does not exist: {$path}");
- }
- return FileSystem::convertPath($path);
- }
}
diff --git a/src/StaticPHP/Skeleton/ArtifactGenerator.php b/src/StaticPHP/Skeleton/ArtifactGenerator.php
index 031d095b..e05f94d1 100644
--- a/src/StaticPHP/Skeleton/ArtifactGenerator.php
+++ b/src/StaticPHP/Skeleton/ArtifactGenerator.php
@@ -1,11 +1,21 @@
source;
}
- public function generateConfig(): array
+ public function setBinary(string $os, array $config): static
+ {
+ $clone = clone $this;
+ if ($clone->binary === null) {
+ $clone->binary = [$os => $config];
+ } else {
+ $clone->binary[$os] = $config;
+ }
+ return $clone;
+ }
+
+ public function generateConfigArray(): array
{
$config = [];
if ($this->source) {
$config['source'] = $this->source;
}
+ if ($this->binary) {
+ $config['binary'] = $this->binary;
+ }
return $config;
}
+
+ public function setConfigFile(string $file): static
+ {
+ $clone = clone $this;
+ $clone->config_file = $file;
+ return $clone;
+ }
+
+ /**
+ * Write the artifact configuration to the config file.
+ */
+ public function writeConfigFile(): string
+ {
+ if ($this->config_file === null) {
+ throw new ValidationException('Config file path is not set.');
+ }
+ $config_array = $this->generateConfigArray();
+ $config_file_json = json_decode(FileSystem::readFile($this->config_file), true);
+ if (!is_array($config_file_json)) {
+ throw new ValidationException('Existing config file is not a valid JSON array.');
+ }
+
+ $config_file_json[$this->name] = $config_array;
+ // sort keys
+ ksort($config_file_json);
+ $json_content = json_encode($config_file_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+ if ($json_content === false) {
+ throw new ValidationException('Failed to encode config array to JSON.');
+ }
+ if (file_put_contents($this->config_file, $json_content) === false) {
+ throw new FileSystemException("Failed to write config file: {$this->config_file}");
+ }
+ return $this->config_file;
+ }
}
diff --git a/src/StaticPHP/Skeleton/ExecutorGenerator.php b/src/StaticPHP/Skeleton/ExecutorGenerator.php
index 02edf2eb..69d7a9f8 100644
--- a/src/StaticPHP/Skeleton/ExecutorGenerator.php
+++ b/src/StaticPHP/Skeleton/ExecutorGenerator.php
@@ -1,9 +1,14 @@
class) {
+ UnixCMakeExecutor::class => [UnixCMakeExecutor::class, 'UnixCMakeExecutor::create($package)->build();'],
+ UnixAutoconfExecutor::class => [UnixAutoconfExecutor::class, 'UnixAutoconfExecutor::create($package)->build();'],
+ WindowsCMakeExecutor::class => [WindowsCMakeExecutor::class, 'WindowsCMakeExecutor::create($package)->build();'],
+ default => throw new ValidationException("Unsupported executor class: {$this->class}"),
+ };
+ }
}
diff --git a/src/StaticPHP/Skeleton/PackageGenerator.php b/src/StaticPHP/Skeleton/PackageGenerator.php
index 95d3c87f..89802899 100644
--- a/src/StaticPHP/Skeleton/PackageGenerator.php
+++ b/src/StaticPHP/Skeleton/PackageGenerator.php
@@ -1,33 +1,49 @@
$depends An array of dependencies required by the package, categorized by operating system. */
+ /** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $depends An array of dependencies required by the package, categorized by operating system. */
protected array $depends = [];
- /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $suggests An array of suggested packages for the package, categorized by operating system. */
+ /** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $suggests An array of suggested packages for the package, categorized by operating system. */
protected array $suggests = [];
/** @var array $frameworks An array of macOS frameworks for the package */
protected array $frameworks = [];
- /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $static_libs An array of static libraries required by the package, categorized by operating system. */
+ /** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $static_libs An array of static libraries required by the package, categorized by operating system. */
protected array $static_libs = [];
- /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $headers An array of header files required by the package, categorized by operating system. */
+ /** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $headers An array of header files required by the package, categorized by operating system. */
protected array $headers = [];
- /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $static_bins An array of static binaries required by the package, categorized by operating system. */
+ /** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $static_bins An array of static binaries required by the package, categorized by operating system. */
protected array $static_bins = [];
- /** @var ArtifactGenerator|null $artifact Artifact */
+ protected ?string $config_file = null;
+
+ /** @var null|ArtifactGenerator $artifact Artifact */
protected ?ArtifactGenerator $artifact = null;
/** @var array $licenses Licenses */
@@ -44,28 +60,28 @@ class PackageGenerator
protected array $func_executor_binding = [];
/**
- * @param string $package_name Package name
- * @param 'library'|'target'|'virtual-target'|'php-extension' $type Package type ('library', 'target', 'virtual-target', etc.)
+ * @param string $package_name Package name
+ * @param 'library'|'php-extension'|'target'|'virtual-target' $type Package type ('library', 'target', 'virtual-target', etc.)
*/
public function __construct(protected string $package_name, protected string $type) {}
/**
* Add package dependency.
*
- * @param string $package Package name
- * @param string $os Operating system ('' for all OSes, '@unix', '@windows', '@macos')
+ * @param string $package Package name
+ * @param string $os_category Operating system ('' for all OSes, 'unix', 'windows', 'macos')
*/
- public function addDependency(string $package, string $os = ''): static
+ public function addDependency(string $package, string $os_category = ''): static
{
- if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) {
- throw new ValidationException("Invalid OS suffix: {$os}");
+ if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) {
+ throw new ValidationException("Invalid OS suffix: {$os_category}");
}
$clone = clone $this;
- if (!isset($clone->depends[$os])) {
- $clone->depends[$os] = [];
+ if (!isset($clone->depends[$os_category])) {
+ $clone->depends[$os_category] = [];
}
- if (!in_array($package, $clone->depends[$os], true)) {
- $clone->depends[$os][] = $package;
+ if (!in_array($package, $clone->depends[$os_category], true)) {
+ $clone->depends[$os_category][] = $package;
}
return $clone;
}
@@ -73,78 +89,78 @@ class PackageGenerator
/**
* Add package suggestion.
*
- * @param string $package Package name
- * @param string $os Operating system ('' for all OSes, '@unix', '@windows', '@macos')
+ * @param string $package Package name
+ * @param string $os_category Operating system ('' for all OSes)
*/
- public function addSuggestion(string $package, string $os = ''): static
+ public function addSuggestion(string $package, string $os_category = ''): static
{
- if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) {
- throw new ValidationException("Invalid OS suffix: {$os}");
+ if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) {
+ throw new ValidationException("Invalid OS suffix: {$os_category}");
}
$clone = clone $this;
- if (!isset($clone->suggests[$os])) {
- $clone->suggests[$os] = [];
+ if (!isset($clone->suggests[$os_category])) {
+ $clone->suggests[$os_category] = [];
}
- if (!in_array($package, $clone->suggests[$os], true)) {
- $clone->suggests[$os][] = $package;
+ if (!in_array($package, $clone->suggests[$os_category], true)) {
+ $clone->suggests[$os_category][] = $package;
}
return $clone;
}
- public function addStaticLib(string $lib_a, string $os = ''): static
+ public function addStaticLib(string $lib_a, string $os_category = ''): static
{
- if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) {
- throw new ValidationException("Invalid OS suffix: {$os}");
+ if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) {
+ throw new ValidationException("Invalid OS suffix: {$os_category}");
}
if (!str_ends_with($lib_a, '.lib') && !str_ends_with($lib_a, '.a')) {
throw new ValidationException("Static library must end with .lib or .a, got: {$lib_a}");
}
- if (str_ends_with($lib_a, '.lib') && in_array($os, ['unix', 'linux', 'macos'], true)) {
+ if (str_ends_with($lib_a, '.lib') && in_array($os_category, ['unix', 'linux', 'macos'], true)) {
throw new ValidationException("Static library with .lib extension cannot be added for non-Windows OS: {$lib_a}");
}
- if (str_ends_with($lib_a, '.a') && $os === 'windows') {
+ if (str_ends_with($lib_a, '.a') && $os_category === 'windows') {
throw new ValidationException("Static library with .a extension cannot be added for Windows OS: {$lib_a}");
}
- if (isset($this->static_libs[$os]) && in_array($lib_a, $this->static_libs[$os], true)) {
+ if (isset($this->static_libs[$os_category]) && in_array($lib_a, $this->static_libs[$os_category], true)) {
// already exists
return $this;
}
$clone = clone $this;
- if (!isset($clone->static_libs[$os])) {
- $clone->static_libs[$os] = [];
+ if (!isset($clone->static_libs[$os_category])) {
+ $clone->static_libs[$os_category] = [];
}
- if (!in_array($lib_a, $clone->static_libs[$os], true)) {
- $clone->static_libs[$os][] = $lib_a;
+ if (!in_array($lib_a, $clone->static_libs[$os_category], true)) {
+ $clone->static_libs[$os_category][] = $lib_a;
}
return $clone;
}
- public function addHeader(string $header_file, string $os = ''): static
+ public function addHeader(string $header_file, string $os_category = ''): static
{
- if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) {
- throw new ValidationException("Invalid OS suffix: {$os}");
+ if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) {
+ throw new ValidationException("Invalid OS suffix: {$os_category}");
}
$clone = clone $this;
- if (!isset($clone->headers[$os])) {
- $clone->headers[$os] = [];
+ if (!isset($clone->headers[$os_category])) {
+ $clone->headers[$os_category] = [];
}
- if (!in_array($header_file, $clone->headers[$os], true)) {
- $clone->headers[$os][] = $header_file;
+ if (!in_array($header_file, $clone->headers[$os_category], true)) {
+ $clone->headers[$os_category][] = $header_file;
}
return $clone;
}
- public function addStaticBin(string $bin_file, string $os = ''): static
+ public function addStaticBin(string $bin_file, string $os_category = ''): static
{
- if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) {
- throw new ValidationException("Invalid OS suffix: {$os}");
+ if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) {
+ throw new ValidationException("Invalid OS suffix: {$os_category}");
}
$clone = clone $this;
- if (!isset($clone->static_bins[$os])) {
- $clone->static_bins[$os] = [];
+ if (!isset($clone->static_bins[$os_category])) {
+ $clone->static_bins[$os_category] = [];
}
- if (!in_array($bin_file, $clone->static_bins[$os], true)) {
- $clone->static_bins[$os][] = $bin_file;
+ if (!in_array($bin_file, $clone->static_bins[$os_category], true)) {
+ $clone->static_bins[$os_category][] = $bin_file;
}
return $clone;
}
@@ -194,9 +210,9 @@ class PackageGenerator
/**
* Enable build for specific OS.
*
- * @param 'Windows'|'Linux'|'Darwin'|array<'Windows'|'Linux'|'Darwin'> $build_for Build for OS
+ * @param 'Darwin'|'Linux'|'Windows'|array<'Darwin'|'Linux'|'Windows'> $build_for Build for OS
*/
- public function enableBuild(string|array $build_for, ?string $build_function_name = null): static
+ public function enableBuild(array|string $build_for, ?string $build_function_name = null): static
{
$clone = clone $this;
if (is_array($build_for)) {
@@ -205,6 +221,9 @@ class PackageGenerator
}
return $clone;
}
+ if (!in_array($build_for, SUPPORTED_OS_FAMILY, true)) {
+ throw new ValidationException("Unsupported build_for value: {$build_for}");
+ }
$clone->build_for_enables[$build_for] = $build_function_name ?? "buildFor{$build_for}";
return $clone;
}
@@ -212,8 +231,8 @@ class PackageGenerator
/**
* Bind function executor.
*
- * @param string $func_name Function name
- * @param ExecutorGenerator $executor Executor generator
+ * @param string $func_name Function name
+ * @param ExecutorGenerator $executor Executor generator
*/
public function addFunctionExecutorBinding(string $func_name, ExecutorGenerator $executor): static
{
@@ -222,10 +241,73 @@ class PackageGenerator
return $clone;
}
+ public function generatePackageClassFile(string $namespace, bool $uppercase = false): string
+ {
+ $printer = new class extends Printer {
+ public string $indentation = ' ';
+ };
+ $file = new PhpFile();
+ $namespace = $file->setStrictTypes()->addNamespace($namespace);
+
+ $uses = [];
+
+ // class name and package attribute
+ $class_name = str_replace('-', '_', $uppercase ? ucwords($this->package_name, '-') : $this->package_name);
+ $class_attribute = match ($this->type) {
+ 'library' => Library::class,
+ 'php-extension' => Extension::class,
+ 'target', 'virtual-target' => Target::class,
+ };
+ $package_class = match ($this->type) {
+ 'library' => LibraryPackage::class,
+ 'php-extension' => PhpExtensionPackage::class,
+ 'target', 'virtual-target' => TargetPackage::class,
+ };
+ $uses[] = $class_attribute;
+ $uses[] = $package_class;
+ $uses[] = BuildFor::class;
+ $uses[] = PackageInstaller::class;
+
+ foreach ($uses as $use) {
+ $namespace->addUse($use);
+ }
+
+ // add class attribute
+ $class = $namespace->addClass($class_name);
+ $class->addAttribute($class_attribute, [$this->package_name]);
+
+ // add build functions if enabled
+ $funcs = [];
+ foreach ($this->build_for_enables as $os_family => $func_name) {
+ if ($func_name !== null) {
+ $funcs[$func_name][] = $os_family;
+ }
+ }
+ foreach ($funcs as $name => $oss) {
+ $method = $class->addMethod(name: $name ?: 'build')
+ ->setPublic()
+ ->setReturnType('void');
+ // check if function executor is bound
+ if (isset($this->func_executor_binding[$name])) {
+ $executor = $this->func_executor_binding[$name];
+ [$executor_use, $code] = $executor->generateCode();
+ $namespace->addUse($executor_use);
+ $method->setBody($code);
+ }
+ $method->addParameter('package')->setType($package_class);
+ $method->addParameter('installer')->setType(PackageInstaller::class);
+ foreach ($oss as $os) {
+ $method->addAttribute(BuildFor::class, [$os]);
+ }
+ }
+
+ return $printer->printFile($file);
+ }
+
/**
* Generate package config
*/
- public function generateConfig(): array
+ public function generateConfigArray(): array
{
$config = ['type' => $this->type];
@@ -280,4 +362,51 @@ class PackageGenerator
return $config;
}
+
+ public function setConfigFile(string $config_file): static
+ {
+ $clone = clone $this;
+ $clone->config_file = $config_file;
+ return $clone;
+ }
+
+ public function writeConfigFile(): string
+ {
+ if ($this->config_file === null) {
+ throw new ValidationException('Config file path is not set.');
+ }
+ $config_array = $this->generateConfigArray();
+ $config_file_json = json_decode(FileSystem::readFile($this->config_file), true);
+ if (!is_array($config_file_json)) {
+ throw new ValidationException('Existing config file is not a valid JSON array.');
+ }
+ $config_file_json[$this->package_name] = $config_array;
+ ksort($config_file_json);
+ $json_content = json_encode($config_file_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+ if ($json_content === false) {
+ throw new ValidationException('Failed to encode package config to JSON.');
+ }
+ if (file_put_contents($this->config_file, $json_content) === false) {
+ throw new FileSystemException("Failed to write config file: {$this->config_file}");
+ }
+ return $this->config_file;
+ }
+
+ public function writeAll(): array
+ {
+ // write config
+ $package_config_file = $this->writeConfigFile();
+ $artifact_config_file = $this->artifact->writeConfigFile();
+
+ // write class file
+ $package_class_file_content = $this->generatePackageClassFile('StaticPHP\Packages');
+ $package_class_file_path = str_replace('-', '_', $this->package_name) . '.php';
+ // file_put_contents($package_class_file_path, $package_class_file_content); // Uncomment this line to actually write the file
+ return [
+ 'package_config' => $package_config_file,
+ 'artifact_config' => $artifact_config_file,
+ 'package_class_file' => $package_class_file_path,
+ 'package_class_content' => $package_class_file_content,
+ ];
+ }
}
diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php
index 2f540d70..1c21a92b 100644
--- a/src/StaticPHP/Util/FileSystem.php
+++ b/src/StaticPHP/Util/FileSystem.php
@@ -472,6 +472,23 @@ class FileSystem
return file_put_contents($file, implode('', $lines));
}
+ /**
+ * Return full path, resolving relative paths against a base path.
+ *
+ * @param string $path Input path (relative or absolute)
+ * @param string $relative_path_base Base path for relative paths
+ */
+ public static function fullpath(string $path, string $relative_path_base): string
+ {
+ if (FileSystem::isRelativePath($path)) {
+ $path = $relative_path_base . DIRECTORY_SEPARATOR . $path;
+ }
+ if (!file_exists($path)) {
+ throw new FileSystemException("Path does not exist: {$path}");
+ }
+ return FileSystem::convertPath($path);
+ }
+
private static function replaceFile(string $filename, int $replace_type = REPLACE_FILE_STR, mixed $callback_or_search = null, mixed $to_replace = null): false|int
{
logger()->debug('Replacing file with type[' . $replace_type . ']: ' . $filename);
diff --git a/src/StaticPHP/Util/InteractiveTerm.php b/src/StaticPHP/Util/InteractiveTerm.php
index 1682ed1f..0570f31c 100644
--- a/src/StaticPHP/Util/InteractiveTerm.php
+++ b/src/StaticPHP/Util/InteractiveTerm.php
@@ -52,6 +52,7 @@ class InteractiveTerm
default => logger()->info(strip_ansi_colors($message)),
};
} else {
+ $output = $level === 'error' && $output instanceof ConsoleOutput ? $output->getErrorOutput() : $output;
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')($message));
}
}
diff --git a/src/globals/defines.php b/src/globals/defines.php
index 3e6d2360..dbcb63f2 100644
--- a/src/globals/defines.php
+++ b/src/globals/defines.php
@@ -96,13 +96,32 @@ const SPC_STATUS_ALREADY_BUILT = 1;
const SPC_DOWNLOAD_TYPE_DISPLAY_NAME = [
'bitbuckettag' => 'BitBucket',
- 'filelist' => 'website',
+ 'filelist' => 'File index website',
'git' => 'git',
'ghrel' => 'GitHub release',
- 'ghtar', 'ghtagtar' => 'GitHub tarball',
+ 'ghtar' => 'GitHub release tarball',
+ 'ghtagtar' => 'GitHub tag tarball',
'local' => 'local dir',
- 'pie' => 'PHP Installer for Extensions',
+ 'pie' => 'PHP Installer for Extensions (PIE)',
'url' => 'url',
'php-release' => 'php.net',
'custom' => 'custom downloader',
];
+
+const SUPPORTED_OS_CATEGORY = [
+ 'unix',
+ 'windows',
+ 'linux',
+ 'macos',
+];
+
+const SUPPORTED_OS_FAMILY = [
+ 'Linux',
+ 'Darwin',
+ 'Windows',
+];
+
+const SPC_MODE_SOURCE = 1;
+const SPC_MODE_VENDOR = 2;
+const SPC_MODE_PHAR = 4;
+const SPC_MODE_VENDOR_PHAR = SPC_MODE_VENDOR | SPC_MODE_PHAR;
diff --git a/src/globals/functions.php b/src/globals/functions.php
index 93cd1ae0..bb22f3a7 100644
--- a/src/globals/functions.php
+++ b/src/globals/functions.php
@@ -10,6 +10,31 @@ use StaticPHP\Runtime\Shell\UnixShell;
use StaticPHP\Runtime\Shell\WindowsCmd;
use ZM\Logger\ConsoleLogger;
+/**
+ * Get the current SPC loading mode. If passed a mode to check, will return whether current mode matches the given mode.
+ */
+function spc_mode(?int $check_mode = null): bool|int
+{
+ $mode = SPC_MODE_SOURCE;
+ // if current file is in phar, then it's phar mode
+ if (str_starts_with(__FILE__, 'phar://') && Phar::running()) {
+ // judge whether it's vendor mode (inside vendor/) or source mode (inside src/)
+ if (basename(dirname(__FILE__, 3)) === 'static-php-cli' && basename(dirname(__FILE__, 5)) === 'vendor') {
+ $mode = SPC_MODE_VENDOR_PHAR;
+ } else {
+ $mode = SPC_MODE_PHAR;
+ }
+ } elseif (basename(dirname(__FILE__, 3)) === 'static-php-cli' && basename(dirname(__FILE__, 5)) === 'vendor') {
+ $mode = SPC_MODE_VENDOR;
+ }
+
+ if ($check_mode === null) {
+ return $mode;
+ }
+ // use bitwise AND to check mode
+ return ($mode & $check_mode) !== 0;
+}
+
/**
* Judge if an array is an associative array
*/