diff --git a/src/StaticPHP/Command/CraftCommand.php b/src/StaticPHP/Command/CraftCommand.php
new file mode 100644
index 00000000..9b1126ac
--- /dev/null
+++ b/src/StaticPHP/Command/CraftCommand.php
@@ -0,0 +1,224 @@
+addArgument('craft', null, 'Path to craft.yml file', WORKING_DIR . '/craft.yml');
+ }
+
+ public function handle(): int
+ {
+ $craft_file = $this->getArgument('craft');
+ if (!file_exists($craft_file)) {
+ $this->output->writeln('craft.yml not found, please create one!');
+ return static::USER_ERROR;
+ }
+
+ $craft = $this->validateAndParseCraftFile($craft_file);
+
+ // set verbosity
+ $this->output->setVerbosity($craft['verbosity']);
+
+ // apply env
+ array_walk($craft['extra-env'], fn ($v, $k) => f_putenv("{$k}={$v}"));
+
+ // run doctor
+ if ($craft['craft-options']['doctor']) {
+ $doctor = new Doctor($this->output, FIX_POLICY_AUTOFIX);
+ if ($doctor->checkAll()) {
+ Doctor::markPassed();
+ $this->output->writeln('');
+ } else {
+ $this->output->writeln('Doctor check failed, please fix the issues and try again.');
+ return static::ENVIRONMENT_ERROR;
+ }
+ }
+
+ // parse download-options to installer's dl options
+ $build_options = $craft['build-options'];
+ if (!$craft['craft-options']['download']) {
+ $build_options['no-download'] = true;
+ }
+ foreach ($craft['download-options'] as $k => $v) {
+ $build_options["dl-{$k}"] = $v;
+ }
+
+ // parse SAPI
+ foreach ($craft['sapi'] as $name) {
+ $build_options["build-{$name}"] = true;
+ }
+
+ // clean build
+ if ($craft['clean-build']) {
+ FileSystem::resetDir(BUILD_ROOT_PATH);
+ FileSystem::resetDir(SOURCE_PATH);
+ }
+
+ $starttime = microtime(true);
+ // run installer
+ $installer = new PackageInstaller($build_options);
+ $installer->addBuildPackage('php');
+ $installer->run(true);
+
+ $usedtime = round(microtime(true) - $starttime, 1);
+ $this->output->writeln("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
+ $this->output->writeln("✔ BUILD SUCCESSFUL ({$usedtime} s)");
+ $this->output->writeln("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
+
+ $installer->printBuildPackageOutputs();
+
+ return static::SUCCESS;
+ }
+
+ /**
+ * Validate and parse craft.yml file to array.
+ *
+ * @param string $craft_file craft.yml path
+ * @return array{
+ * php-version: string,
+ * extensions: array,
+ * shared-extensions: array,
+ * packages: array,
+ * sapi: array,
+ * verbosity: int,
+ * debug: bool,
+ * clean-build: bool,
+ * build-options: array,
+ * download-options: array,
+ * extra-env: array,
+ * craft-options: array{
+ * doctor: bool,
+ * download: bool,
+ * build: bool
+ * }
+ * } Parsed craft content
+ */
+ private function validateAndParseCraftFile(string $craft_file): array
+ {
+ $build_options = $this->getApplication()->find('build:php')->getDefinition()->getOptions();
+ $download_options = $this->getApplication()->find('download')->getDefinition()->getOptions();
+ try {
+ $craft = Yaml::parseFile($craft_file);
+ } catch (ParseException $e) {
+ throw new ValidationException("Craft file '{$craft_file}' is broken: {$e->getMessage()}");
+ }
+ if (!is_assoc_array($craft)) {
+ throw new ValidationException("Craft file '{$craft_file}' must be an associative array.");
+ }
+
+ // check php-version
+ if (isset($craft['php-version']) && !preg_match('/^(\d+)(\.\d+)?(\.\d+)?$/', strval($craft['php-version']))) {
+ throw new ValidationException("Craft file '{$craft_file}' has invalid 'php-version' field, it should be in format of '8.0.0'.");
+ }
+
+ // check php extensions field
+ if (!isset($craft['extensions'])) {
+ throw new ValidationException("Craft file '{$craft_file}' must have 'extensions' field.");
+ }
+ // parse extension if not list
+ if (is_string($craft['extensions'])) {
+ $craft['extensions'] = parse_extension_list($craft['extensions']);
+ }
+
+ // check shared-extensions field
+ if (!isset($craft['shared-extensions'])) {
+ $craft['shared-extensions'] = [];
+ } elseif (is_string($craft['shared-extensions'])) {
+ $craft['shared-extensions'] = parse_extension_list($craft['shared-extensions']);
+ }
+
+ // check libs and additional packages
+ $v2_libs = parse_comma_list($craft['libs'] ?? []);
+ $v3_packages = parse_comma_list($craft['packages'] ?? []);
+ $craft['packages'] = array_merge($v2_libs, $v3_packages);
+
+ // check PHP SAPI
+ if (!isset($craft['sapi'])) {
+ throw new ValidationException('Craft file "sapi" is required.');
+ }
+ if (is_string($craft['sapi'])) {
+ $craft['sapi'] = parse_comma_list($craft['sapi']);
+ }
+
+ // verbosity
+ $verbosity_level = $craft['verbosity'] ?? OutputInterface::VERBOSITY_NORMAL;
+ $debug = $craft['debug'] ?? false;
+ if ($debug) {
+ $verbosity_level = OutputInterface::VERBOSITY_DEBUG;
+ }
+ $craft['verbosity'] = $verbosity_level;
+
+ // clean-build (if true, reset before all builds)
+ $craft['clean-build'] ??= false;
+
+ // build-options
+ if (isset($craft['build-options'])) {
+ if (!is_assoc_array($craft['build-options'])) {
+ throw new ValidationException('Craft file "build" options must be an associative array.');
+ }
+ foreach ($craft['build-options'] as $key => $value) {
+ if (!isset($build_options[$key])) {
+ throw new ValidationException('Craft file "build" option "' . $key . '" is invalid.');
+ }
+ if ($build_options[$key]->isArray() && !is_array($value)) {
+ throw new ValidationException('Craft file "build" option "' . $key . '" must be an array.');
+ }
+ }
+ } else {
+ $craft['build-options'] = [];
+ }
+
+ // download-options
+ if (isset($craft['download-options'])) {
+ if (!is_assoc_array($craft['download-options'])) {
+ throw new ValidationException('Craft file "download" options must be an associative array.');
+ }
+ foreach ($craft['download-options'] as $key => $value) {
+ if (!isset($download_options[$key])) {
+ throw new ValidationException('Craft file "download" option "' . $key . '" is invalid.');
+ }
+ if ($download_options[$key]->isArray() && !is_array($value)) {
+ throw new ValidationException('Craft file "download" option "' . $key . '" must be an array.');
+ }
+ }
+ } else {
+ $craft['download-options'] = [];
+ }
+
+ // post-parse: parse php-version field to download options
+ if (isset($craft['php-version'])) {
+ $craft['download-options']['with-php'] = strval($craft['php-version']);
+ $craft['download-options']['ignore-cache'] = (($craft['download-options']['ignore-cache'] ?? false) === true ? true : 'php-src');
+ }
+
+ // extra-env
+ if (isset($craft['extra-env'])) {
+ if (!is_assoc_array($craft['extra-env'])) {
+ throw new ValidationException('Craft file "extra-env" must be an associative array.');
+ }
+ } else {
+ $craft['extra-env'] = [];
+ }
+
+ // craft-options
+ $craft['craft-options']['doctor'] ??= true;
+ $craft['craft-options']['download'] ??= true;
+
+ return $craft;
+ }
+}
diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php
index 0c8fec83..21e3ee8b 100644
--- a/src/StaticPHP/ConsoleApplication.php
+++ b/src/StaticPHP/ConsoleApplication.php
@@ -7,6 +7,7 @@ namespace StaticPHP;
use StaticPHP\Command\BuildLibsCommand;
use StaticPHP\Command\BuildTargetCommand;
use StaticPHP\Command\CheckUpdateCommand;
+use StaticPHP\Command\CraftCommand;
use StaticPHP\Command\Dev\DumpCapabilitiesCommand;
use StaticPHP\Command\Dev\DumpStagesCommand;
use StaticPHP\Command\Dev\EnvCommand;
@@ -67,6 +68,7 @@ class ConsoleApplication extends Application
new ResetCommand(),
new CheckUpdateCommand(),
new MicroCombineCommand(),
+ new CraftCommand(),
// dev commands
new ShellCommand(),