Skip to content

Commit 564e204

Browse files
authored
V3 refactor/craft (#1091)
2 parents ec39905 + bca1fb5 commit 564e204

8 files changed

Lines changed: 256 additions & 66 deletions

File tree

TODO.md

Lines changed: 0 additions & 57 deletions
This file was deleted.

config/pkg/ext/ext-deepclone.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
ext-deepclone:
2+
type: php-extension
3+
artifact:
4+
source:
5+
type: ghtagtar
6+
repo: symfony/php-ext-deepclone
7+
extract: php-src/ext/deepclone
8+
metadata:
9+
license-files: [LICENSE]
10+
license: PHP-3.01
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace StaticPHP\Command;
6+
7+
use StaticPHP\Doctor\Doctor;
8+
use StaticPHP\Exception\ValidationException;
9+
use StaticPHP\Package\PackageInstaller;
10+
use StaticPHP\Util\FileSystem;
11+
use Symfony\Component\Console\Attribute\AsCommand;
12+
use Symfony\Component\Console\Output\OutputInterface;
13+
use Symfony\Component\Yaml\Exception\ParseException;
14+
use Symfony\Component\Yaml\Yaml;
15+
16+
#[AsCommand('craft', 'Build static-php from craft.yml')]
17+
class CraftCommand extends BaseCommand
18+
{
19+
public function configure(): void
20+
{
21+
$this->addArgument('craft', null, 'Path to craft.yml file', WORKING_DIR . '/craft.yml');
22+
}
23+
24+
public function handle(): int
25+
{
26+
$craft_file = $this->getArgument('craft');
27+
if (!file_exists($craft_file)) {
28+
$this->output->writeln('<error>craft.yml not found, please create one!</error>');
29+
return static::USER_ERROR;
30+
}
31+
32+
$craft = $this->validateAndParseCraftFile($craft_file);
33+
34+
// set verbosity
35+
$this->output->setVerbosity($craft['verbosity']);
36+
37+
// apply env
38+
array_walk($craft['extra-env'], fn ($v, $k) => f_putenv("{$k}={$v}"));
39+
40+
// run doctor
41+
if ($craft['craft-options']['doctor']) {
42+
$doctor = new Doctor($this->output, FIX_POLICY_AUTOFIX);
43+
if ($doctor->checkAll()) {
44+
Doctor::markPassed();
45+
$this->output->writeln('');
46+
} else {
47+
$this->output->writeln('<error>Doctor check failed, please fix the issues and try again.</error>');
48+
return static::ENVIRONMENT_ERROR;
49+
}
50+
}
51+
52+
// parse download-options to installer's dl options
53+
$build_options = $craft['build-options'];
54+
if (!$craft['craft-options']['download']) {
55+
$build_options['no-download'] = true;
56+
}
57+
foreach ($craft['download-options'] as $k => $v) {
58+
$build_options["dl-{$k}"] = $v;
59+
}
60+
61+
// parse SAPI
62+
foreach ($craft['sapi'] as $name) {
63+
$build_options["build-{$name}"] = true;
64+
}
65+
66+
// clean build
67+
if ($craft['clean-build']) {
68+
FileSystem::resetDir(BUILD_ROOT_PATH);
69+
FileSystem::resetDir(SOURCE_PATH);
70+
}
71+
72+
$starttime = microtime(true);
73+
// run installer
74+
$installer = new PackageInstaller($build_options);
75+
$installer->addBuildPackage('php');
76+
$installer->run(true);
77+
78+
$usedtime = round(microtime(true) - $starttime, 1);
79+
$this->output->writeln("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
80+
$this->output->writeln("<info>✔ BUILD SUCCESSFUL ({$usedtime} s)</info>");
81+
$this->output->writeln("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
82+
83+
$installer->printBuildPackageOutputs();
84+
85+
return static::SUCCESS;
86+
}
87+
88+
/**
89+
* Validate and parse craft.yml file to array.
90+
*
91+
* @param string $craft_file craft.yml path
92+
* @return array{
93+
* php-version: string,
94+
* extensions: array<string>,
95+
* shared-extensions: array<string>,
96+
* packages: array<string>,
97+
* sapi: array<string>,
98+
* verbosity: int,
99+
* debug: bool,
100+
* clean-build: bool,
101+
* build-options: array<string, mixed>,
102+
* download-options: array<string, mixed>,
103+
* extra-env: array<string, string>,
104+
* craft-options: array{
105+
* doctor: bool,
106+
* download: bool,
107+
* build: bool
108+
* }
109+
* } Parsed craft content
110+
*/
111+
private function validateAndParseCraftFile(string $craft_file): array
112+
{
113+
$build_options = $this->getApplication()->find('build:php')->getDefinition()->getOptions();
114+
$download_options = $this->getApplication()->find('download')->getDefinition()->getOptions();
115+
try {
116+
$craft = Yaml::parseFile($craft_file);
117+
} catch (ParseException $e) {
118+
throw new ValidationException("Craft file '{$craft_file}' is broken: {$e->getMessage()}");
119+
}
120+
if (!is_assoc_array($craft)) {
121+
throw new ValidationException("Craft file '{$craft_file}' must be an associative array.");
122+
}
123+
124+
// check php-version
125+
if (isset($craft['php-version']) && !preg_match('/^(\d+)(\.\d+)?(\.\d+)?$/', strval($craft['php-version']))) {
126+
throw new ValidationException("Craft file '{$craft_file}' has invalid 'php-version' field, it should be in format of '8.0.0'.");
127+
}
128+
129+
// check php extensions field
130+
if (!isset($craft['extensions'])) {
131+
throw new ValidationException("Craft file '{$craft_file}' must have 'extensions' field.");
132+
}
133+
// parse extension if not list
134+
if (is_string($craft['extensions'])) {
135+
$craft['extensions'] = parse_extension_list($craft['extensions']);
136+
}
137+
138+
// check shared-extensions field
139+
if (!isset($craft['shared-extensions'])) {
140+
$craft['shared-extensions'] = [];
141+
} elseif (is_string($craft['shared-extensions'])) {
142+
$craft['shared-extensions'] = parse_extension_list($craft['shared-extensions']);
143+
}
144+
145+
// check libs and additional packages
146+
$v2_libs = parse_comma_list($craft['libs'] ?? []);
147+
$v3_packages = parse_comma_list($craft['packages'] ?? []);
148+
$craft['packages'] = array_merge($v2_libs, $v3_packages);
149+
150+
// check PHP SAPI
151+
if (!isset($craft['sapi'])) {
152+
throw new ValidationException('Craft file "sapi" is required.');
153+
}
154+
if (is_string($craft['sapi'])) {
155+
$craft['sapi'] = parse_comma_list($craft['sapi']);
156+
}
157+
158+
// verbosity
159+
$verbosity_level = $craft['verbosity'] ?? OutputInterface::VERBOSITY_NORMAL;
160+
$debug = $craft['debug'] ?? false;
161+
if ($debug) {
162+
$verbosity_level = OutputInterface::VERBOSITY_DEBUG;
163+
}
164+
$craft['verbosity'] = $verbosity_level;
165+
166+
// clean-build (if true, reset before all builds)
167+
$craft['clean-build'] ??= false;
168+
169+
// build-options
170+
if (isset($craft['build-options'])) {
171+
if (!is_assoc_array($craft['build-options'])) {
172+
throw new ValidationException('Craft file "build" options must be an associative array.');
173+
}
174+
foreach ($craft['build-options'] as $key => $value) {
175+
if (!isset($build_options[$key])) {
176+
throw new ValidationException('Craft file "build" option "' . $key . '" is invalid.');
177+
}
178+
if ($build_options[$key]->isArray() && !is_array($value)) {
179+
throw new ValidationException('Craft file "build" option "' . $key . '" must be an array.');
180+
}
181+
}
182+
} else {
183+
$craft['build-options'] = [];
184+
}
185+
186+
// download-options
187+
if (isset($craft['download-options'])) {
188+
if (!is_assoc_array($craft['download-options'])) {
189+
throw new ValidationException('Craft file "download" options must be an associative array.');
190+
}
191+
foreach ($craft['download-options'] as $key => $value) {
192+
if (!isset($download_options[$key])) {
193+
throw new ValidationException('Craft file "download" option "' . $key . '" is invalid.');
194+
}
195+
if ($download_options[$key]->isArray() && !is_array($value)) {
196+
throw new ValidationException('Craft file "download" option "' . $key . '" must be an array.');
197+
}
198+
}
199+
} else {
200+
$craft['download-options'] = [];
201+
}
202+
203+
// post-parse: parse php-version field to download options
204+
if (isset($craft['php-version'])) {
205+
$craft['download-options']['with-php'] = strval($craft['php-version']);
206+
$craft['download-options']['ignore-cache'] = (($craft['download-options']['ignore-cache'] ?? false) === true ? true : 'php-src');
207+
}
208+
209+
// extra-env
210+
if (isset($craft['extra-env'])) {
211+
if (!is_assoc_array($craft['extra-env'])) {
212+
throw new ValidationException('Craft file "extra-env" must be an associative array.');
213+
}
214+
} else {
215+
$craft['extra-env'] = [];
216+
}
217+
218+
// craft-options
219+
$craft['craft-options']['doctor'] ??= true;
220+
$craft['craft-options']['download'] ??= true;
221+
222+
return $craft;
223+
}
224+
}

src/StaticPHP/Command/ResetCommand.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ public function handle(): int
6262
InteractiveTerm::indicateProgress("Removing: {$path}");
6363

6464
if (PHP_OS_FAMILY === 'Windows') {
65+
Shell::passthruCallback(fn () => InteractiveTerm::advance());
6566
// Force delete on Windows to handle git directories
6667
$this->removeDirectoryWindows($path);
68+
Shell::passthruCallback(null);
6769
} else {
6870
// Use FileSystem::removeDir for Unix systems
6971
FileSystem::removeDir($path);
@@ -88,7 +90,6 @@ private function removeDirectoryWindows(string $path): void
8890

8991
// Try using PowerShell for force deletion
9092
$escaped_path = escapeshellarg($path);
91-
Shell::passthruCallback(fn () => InteractiveTerm::advance());
9293

9394
// Use PowerShell Remove-Item with -Force and -Recurse
9495
$result = cmd()->execWithResult("powershell -Command \"Remove-Item -Path {$escaped_path} -Recurse -Force -ErrorAction SilentlyContinue\"", false);
@@ -106,6 +107,5 @@ private function removeDirectoryWindows(string $path): void
106107
if (is_dir($path)) {
107108
FileSystem::removeDir($path);
108109
}
109-
Shell::passthruCallback(null);
110110
}
111111
}

src/StaticPHP/ConsoleApplication.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use StaticPHP\Command\BuildLibsCommand;
88
use StaticPHP\Command\BuildTargetCommand;
99
use StaticPHP\Command\CheckUpdateCommand;
10+
use StaticPHP\Command\CraftCommand;
1011
use StaticPHP\Command\Dev\DumpCapabilitiesCommand;
1112
use StaticPHP\Command\Dev\DumpStagesCommand;
1213
use StaticPHP\Command\Dev\EnvCommand;
@@ -67,6 +68,7 @@ public function __construct()
6768
new ResetCommand(),
6869
new CheckUpdateCommand(),
6970
new MicroCombineCommand(),
71+
new CraftCommand(),
7072

7173
// dev commands
7274
new ShellCommand(),

src/StaticPHP/Package/PackageInstaller.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,10 @@ public function run(bool $disable_delay_msg = false): void
154154
$this->resolvePackages();
155155
}
156156

157-
if ($this->interactive && !$disable_delay_msg) {
158-
// show install or build options in terminal with beautiful output
159-
$this->printInstallerInfo();
157+
// show install or build options in terminal with beautiful output
158+
$this->printInstallerInfo();
160159

160+
if ($this->interactive && !$disable_delay_msg) {
161161
InteractiveTerm::notice('Build process will start after 2s ...' . PHP_EOL);
162162
sleep(2);
163163
}

src/StaticPHP/Package/TargetPackage.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ public function getBuildOption(string $key, mixed $default = null): mixed
8080
if ($input !== null && $input->hasOption($key)) {
8181
return $input->getOption($key);
8282
}
83+
84+
// try builder options
85+
$builder = ApplicationContext::has(PackageBuilder::class)
86+
? ApplicationContext::get(PackageBuilder::class)
87+
: null;
88+
if ($builder !== null && ($option = $builder->getOption($key)) !== null) {
89+
return $option;
90+
}
8391
return $default;
8492
}
8593

src/StaticPHP/Util/FileSystem.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,10 +287,11 @@ public static function getClassesPsr4(string $dir, string $base_namespace, mixed
287287
/**
288288
* Remove directory recursively
289289
*
290-
* @param string $dir Directory to remove
291-
* @return bool Success status
290+
* @param string $dir Directory to remove
291+
* @param null|callable $callback Callback for every single scan items
292+
* @return bool Success status
292293
*/
293-
public static function removeDir(string $dir): bool
294+
public static function removeDir(string $dir, ?callable $callback = null): bool
294295
{
295296
$dir = self::convertPath($dir);
296297
logger()->debug('Removing path recursively: "' . $dir . '"');
@@ -311,7 +312,9 @@ public static function removeDir(string $dir): bool
311312
}
312313
// 遍历目录
313314
foreach ($scan_list as $v) {
314-
InteractiveTerm::advance();
315+
if ($callback) {
316+
$callback($v);
317+
}
315318
// Unix 系统排除这俩目录
316319
if ($v == '.' || $v == '..') {
317320
continue;

0 commit comments

Comments
 (0)