diff --git a/.github/actions/install/action.yaml b/.github/actions/install/action.yaml index 316eaac8..bc141e4f 100644 --- a/.github/actions/install/action.yaml +++ b/.github/actions/install/action.yaml @@ -18,6 +18,7 @@ runs: uses: shivammathur/setup-php@v2 with: php-version: '${{ inputs.php-version }}' + coverage: none - name: Install dependencies (project) run: composer install --prefer-dist --no-progress --optimize-autoloader --classmap-authoritative ${{ inputs.composer-flags }} diff --git a/.gitignore b/.gitignore index fc4a0f87..a184ceaf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ .castor.stub.php /.php-cs-fixer.cache /.phpunit.cache -/castor.* /my-app.* /var/ /vendor/ /tests/Examples/fixtures/**/.castor.stub.php +/castor.* +!/castor.php +!/castor.composer.json +!/castor.composer.lock diff --git a/castor.composer.json b/castor.composer.json new file mode 100644 index 00000000..9ef5ca31 --- /dev/null +++ b/castor.composer.json @@ -0,0 +1,31 @@ +{ + "config": { + "sort-packages": true + }, + "replace": { + "castor\/castor": "v0.15.0" + }, + "require": { + "pyrech\/castor-example": "^1.0", + "pyrech\/castor-example-package-not-published": "*", + "pyrech\/foobar": "v1" + }, + "repositories": [ + { + "type": "vcs", + "url": "https:\/\/github.com\/pyrech\/castor-example-package-not-published.git" + }, + { + "type": "package", + "package": { + "name": "pyrech\/foobar", + "version": "v1", + "source": { + "url": "https:\/\/github.com\/pyrech\/castor-example-misc.git", + "type": "git", + "reference": "main" + } + } + } + ] +} diff --git a/castor.composer.lock b/castor.composer.lock new file mode 100644 index 00000000..fa6d7ab2 --- /dev/null +++ b/castor.composer.lock @@ -0,0 +1,78 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "b75aac45797c9e71f42258d681d262de", + "packages": [ + { + "name": "pyrech/castor-example", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/pyrech/castor-example.git", + "reference": "775447566dec8b7e5f73664fb34b98b7843d2f48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pyrech/castor-example/zipball/775447566dec8b7e5f73664fb34b98b7843d2f48", + "reference": "775447566dec8b7e5f73664fb34b98b7843d2f48", + "shasum": "" + }, + "type": "project", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "issues": "https://github.com/pyrech/castor-example/issues", + "source": "https://github.com/pyrech/castor-example/tree/v1.1.0" + }, + "time": "2024-02-26T16:09:25+00:00" + }, + { + "name": "pyrech/castor-example-package-not-published", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/pyrech/castor-example-package-not-published.git", + "reference": "e87fa3f71b9784800caadca8ca3e001bb646f201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pyrech/castor-example-package-not-published/zipball/e87fa3f71b9784800caadca8ca3e001bb646f201", + "reference": "e87fa3f71b9784800caadca8ca3e001bb646f201", + "shasum": "" + }, + "type": "project", + "license": [ + "MIT" + ], + "support": { + "source": "https://github.com/pyrech/castor-example-package-not-published/tree/v1.0.0", + "issues": "https://github.com/pyrech/castor-example-package-not-published/issues" + }, + "time": "2024-02-26T13:09:56+00:00" + }, + { + "name": "pyrech/foobar", + "version": "v1", + "source": { + "type": "git", + "url": "https://github.com/pyrech/castor-example-misc.git", + "reference": "main" + }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/doc/going-further/extending-castor/remote-imports.md b/doc/going-further/extending-castor/remote-imports.md index 4bd31651..9d451115 100644 --- a/doc/going-further/extending-castor/remote-imports.md +++ b/doc/going-further/extending-castor/remote-imports.md @@ -1,108 +1,53 @@ # Import remote functions +> [!WARNING] +> Remote imports is in a experimental state and may change in the future. + Castor can import functions from your filesystem but also from a remote resource. -## Importing functions +## Installing remote packages When importing functions from a remote resource, Castor will use Composer to download the packages and store them in `.castor/vendor/`. -To import functions, you need to use the same `import()` function used to import -your tasks, but this time with a different syntax for the `path` argument. - -The import syntax depends on the source of the packages. - -### From a Composer package (scheme `composer://`) - -This is the most common use case when the functions to import are defined in a -Composer package. You can directly import them by using the package name -prefixed with the `composer://` scheme: +To import functions, you need to create a `castor.composer.json` file next to +the `castor.php` file (either at the root of your project or in the `.castor/` +directory). -```php -use function Castor\import; - -import('composer://vendor-name/project-name'); -``` +This also can be done by running the `castor composer init` command. -This will import all the tasks defined in the package. +See the [Composer documentation](https://getcomposer.org/doc/04-schema.md) for +more information about the `composer.json` file. -#### Specify the version +## Importing file from a remote package -You can define the version of the package to import by using the `version` -argument: +Third party functions may not be autoloaded by Composer, as there may be +optional. To import them, you can use the `import()` function. ```php -use function Castor\import; - -import('composer://vendor-name/project-name', version: '^1.0'); +import('composer://vendor/package/', file: 'functions.php'); ``` -You can use any version constraint supported by Composer (like `*`, `dev-main`, -`^1.0`, etc.). See the [Composer documentation](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints) -for more information. +File is optional, if not provided, Castor will look for a `castor.php` file in +the package. -> [!TIP] -> The `version` argument is optional and will default to `*`. - -#### Import from a package not pushed to packagist.org - -In some cases, you may have a Composer package that is not pushed to -packagist.org (like a private package hosted on packagist.com or another package -registry). In such cases, you can import it by using the `vcs` argument to -specify the repository URL where the package is hosted: - -```php - -use function Castor\import; - -import('composer://vendor-name/project-name', vcs: 'https://github.com/organization/repository.git'); -``` +## Manipulating castor composer file -### From a repository (scheme `package://`) +Castor provide a `composer` command to manipulate the `castor.composer.json` +file. -If the functions you want to import are not available as a Composer package, you -can still import them by using a special configuration that Composer will -understand. This will now use the `package://` scheme. +For example, you can use it to add a package to the file: -```php -use function Castor\import; - -import('package://vendor-name/project-name', source: [ - 'url' => 'https://github.com/organization/repository.git', - 'type' => 'git', // 'Any source type supported by Composer (git, svn, etc)' - 'reference' => 'main', // A commit id, a branch or a tag name -]); +```bash +castor composer require 'vendor/package' ``` -> [!NOTE] -> The "vendor-name/project-name" name can be whatever you want, and will only be -> used internally by Castor and Composer to make the repository behave like a -> normal Composer package. - -> [!TIP] -> Rather than using the `package://` scheme, it may be simpler to create a -> standard `composer.json` to your repository and import your newly created -> package by using the `composer://` scheme and the `vcs` argument. - -## Import only a specific file - -No matter where does the package come from (Composer package, git repository, -etc.), you can restrict the file (or directory) to be imported. This is -configured by using the `file` argument specifying the path inside the package -or repository. - -```php -use function Castor\import; +Or you can use it to update packages -import('composer://vendor-name/project-name', file: 'castor/my-tasks.php'); +```bash +castor composer update ``` -> [!NOTE] -> The `file` argument is optional and will empty by default, causing Castor to -> import and parse all the PHP files in the package. While handy, it's probably -> not what you want if your package contains PHP code that are not related to -> Castor. - ## Preventing remote imports In case you have trouble with the imported functions (or if you don't trust @@ -125,19 +70,10 @@ $ export CASTOR_NO_REMOTE=1 $ castor my-task # will not import any remote functions ``` -## Update imported packages - -When you import a package in a given version, Castor will not update -automatically update the packages once a new version of your dependency is -available. +## Lock file -To update your dependencies, you will either need to: +Like every PHP projects using Composer, it will generate a +`castor.composer.lock` file to lock the versions of the imported packages. -- change the required version yourself (thus every one using your Castor project -will profit of the update once they run your project); -- force the update on your side only by either using the `--update-remotes` -option or by removing the `.castor/vendor/` folder. - -```bash -$ castor --update-remotes -``` +It is recommended to commit this file to your version control system to ensure +that everyone uses the same versions of the imported packages. diff --git a/examples/remote-import.php b/examples/remote-import.php index 3d30947e..7d02487c 100644 --- a/examples/remote-import.php +++ b/examples/remote-import.php @@ -7,15 +7,11 @@ use function Castor\import; // Importing tasks from a Composer package -import('composer://pyrech/castor-example', version: '^1.0'); +import('composer://pyrech/castor-example'); // Importing tasks from a Composer package not published on packagist (but still having a composer.json) -import('composer://pyrech/castor-example-package-not-published', version: '*', vcs: 'https://github.com/pyrech/castor-example-package-not-published.git'); +import('composer://pyrech/castor-example-package-not-published'); // Importing tasks from a repository not using Composer -import('package://pyrech/foobar', source: [ - 'url' => 'https://github.com/pyrech/castor-example-misc.git', - 'type' => 'git', - 'reference' => 'main', // commit id, branch or tag name -]); +import('composer://pyrech/foobar'); #[AsTask(description: 'Use functions imported from remote packages')] function remote_tasks(): void diff --git a/src/Console/ApplicationFactory.php b/src/Console/ApplicationFactory.php index 891818af..498e85a8 100644 --- a/src/Console/ApplicationFactory.php +++ b/src/Console/ApplicationFactory.php @@ -3,6 +3,7 @@ namespace Castor\Console; use Castor\Console\Command\CompileCommand; +use Castor\Console\Command\ComposerCommand; use Castor\Console\Command\DebugCommand; use Castor\Console\Command\RepackCommand; use Castor\Container; @@ -199,6 +200,7 @@ private static function configureContainer(ContainerConfigurator $c, bool $repac '$containerBuilder' => service(ContainerInterface::class), ]) ->call('add', [service(DebugCommand::class)]) + ->call('add', [service(ComposerCommand::class)]) ->call('setDispatcher', [service(EventDispatcherInterface::class)]) ->call('setCatchErrors', [true]) ; diff --git a/src/Console/Command/ComposerCommand.php b/src/Console/Command/ComposerCommand.php new file mode 100644 index 00000000..653df8fb --- /dev/null +++ b/src/Console/Command/ComposerCommand.php @@ -0,0 +1,53 @@ +ignoreValidationErrors() + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $extra = array_filter($this->getRawTokens($input), fn ($item) => 'composer' !== $item); + + $vendorDirectory = $this->rootDir . Composer::VENDOR_DIR; + + if (!file_exists($file = $this->rootDir . '/castor.composer.json') && !file_exists($file = $this->rootDir . '/.castor/castor.composer.json')) { + // Default to the root directory (so someone can do a composer init by example) + $file = $this->rootDir . '/castor.composer.json'; + } + + $this->composer->run($file, $vendorDirectory, $extra, $output, true); + + return Command::SUCCESS; + } +} diff --git a/src/Import/Importer.php b/src/Import/Importer.php index 3e1c7586..e7949f1c 100644 --- a/src/Import/Importer.php +++ b/src/Import/Importer.php @@ -26,8 +26,7 @@ public function __construct( ) { } - /** @phpstan-param ImportSource $source */ - public function import(string $path, ?string $file = null, ?string $version = null, ?string $vcs = null, ?array $source = null): void + public function import(string $path, ?string $file = null): void { $scheme = parse_url($path, \PHP_URL_SCHEME); @@ -40,13 +39,10 @@ public function import(string $path, ?string $file = null, ?string $version = nu $package = mb_substr($path, mb_strlen($scheme) + 3); try { - $this->packageImporter->addPackage( + $this->packageImporter->importFromPackage( $scheme, $package, $file, - $version, - $vcs, - $source, ); return; @@ -57,8 +53,8 @@ public function import(string $path, ?string $file = null, ?string $version = nu return; } - } elseif (null !== $file || null !== $version || null !== $vcs || null !== $source) { - throw $this->createImportException($path, 'The "file", "version", "vcs" and "source" arguments can only be used with a remote import.'); + } elseif (null !== $file) { + throw $this->createImportException($path, 'The "file" argument can only be used with a remote import.'); } if (!file_exists($path)) { diff --git a/src/Import/Mount.php b/src/Import/Mount.php index 5997e9db..a6a1669e 100644 --- a/src/Import/Mount.php +++ b/src/Import/Mount.php @@ -12,6 +12,7 @@ public function __construct( public readonly string $path, public readonly bool $allowEmptyEntrypoint = false, public readonly ?string $namespacePrefix = null, + public readonly bool $allowRemotePackage = true, ) { } } diff --git a/src/Import/Remote/Composer.php b/src/Import/Remote/Composer.php index 4802dbea..6c680375 100644 --- a/src/Import/Remote/Composer.php +++ b/src/Import/Remote/Composer.php @@ -2,7 +2,6 @@ namespace Castor\Import\Remote; -use Castor\Console\Application; use Castor\Helper\PathHelper; use Castor\Import\Exception\ComposerError; use Composer\Console\Application as ComposerApplication; @@ -18,71 +17,54 @@ class Composer { public const VENDOR_DIR = '/.castor/vendor/'; - public const DEFAULT_COMPOSER_CONFIGURATION = [ - 'description' => 'This file is managed by Castor. Do not edit it manually.', - 'config' => [ - 'sort-packages' => true, - 'vendor-dir' => '.', - ], - 'replace' => [ - 'castor/castor' => Application::VERSION, - ], - ]; public function __construct( private readonly Filesystem $filesystem, private readonly OutputInterface $output, - /** @var array */ - private array $configuration = self::DEFAULT_COMPOSER_CONFIGURATION, private readonly LoggerInterface $logger = new NullLogger(), ) { } - /** - * @return array - */ - public function getConfiguration(): array + public function install(string $entrypointDirectory, bool $update = false, bool $displayProgress = true): void { - return $this->configuration; - } + $vendorDirectory = $entrypointDirectory . self::VENDOR_DIR; - /** - * @param array $configuration - */ - public function setConfiguration(array $configuration): void - { - $this->configuration = $configuration; - } + if (!file_exists($file = $entrypointDirectory . '/castor.composer.json') && !file_exists($file = $entrypointDirectory . '/.castor/castor.composer.json')) { + $this->logger->debug(sprintf('The castor.composer.json file does not exists in %s or %s/.castor, skipping composer install.', $entrypointDirectory, $entrypointDirectory)); - public function update(bool $force = false, bool $displayProgress = true): void - { - $dir = PathHelper::getRoot() . self::VENDOR_DIR; + return; + } - if ($force || !$this->isInstalled($dir)) { - $this->filesystem->mkdir($dir); + if (!$update && $this->isInstalled($vendorDirectory, $file)) { + return; + } - file_put_contents($dir . '.gitignore', "*\n"); - file_put_contents("{$dir}/composer.json", json_encode($this->configuration, \JSON_PRETTY_PRINT | \JSON_THROW_ON_ERROR)); + if (!file_exists($vendorDirectory)) { + $this->filesystem->mkdir($vendorDirectory); + } - $progressIndicator = null; - if ($displayProgress) { - $progressIndicator = new ProgressIndicator($this->output, null, 100, ['⠏', '⠛', '⠹', '⢸', '⣰', '⣤', '⣆', '⡇']); - $progressIndicator->start('Downloading remote packages'); - } + file_put_contents($vendorDirectory . '.gitignore', "*\n"); - $this->run(['update'], callback: function () use ($progressIndicator) { - if ($progressIndicator) { - $progressIndicator->advance(); - } - }); + $progressIndicator = null; + + if ($displayProgress) { + $progressIndicator = new ProgressIndicator($this->output, null, 100, ['⠏', '⠛', '⠹', '⢸', '⣰', '⣤', '⣆', '⡇']); + $progressIndicator->start('Downloading remote packages'); + } + $command = $update ? 'update' : 'install'; + + $this->run($file, $vendorDirectory, [$command], callback: function () use ($progressIndicator) { if ($progressIndicator) { - $progressIndicator->finish('Remote packages imported'); + $progressIndicator->advance(); } - $this->writeInstalled($dir); - } else { - $this->logger->debug('Packages were already required, no need to run Composer.'); + }); + + if ($progressIndicator) { + $progressIndicator->finish('Remote packages imported'); } + + $this->writeInstalled($vendorDirectory, $file); } public function remove(): void @@ -93,13 +75,18 @@ public function remove(): void /** * @param string[] $args */ - private function run(array $args, callable $callback): void + public function run(string $composerJsonFilePath, string $vendorDirectory, array $args, callable|OutputInterface $callback, bool $allowInteraction = false): void { - $directory = PathHelper::getRoot() . self::VENDOR_DIR; - $args[] = '--working-dir'; - $args[] = $directory; - $args[] = '--no-interaction'; + $args[] = \dirname($vendorDirectory); + + if (!$allowInteraction) { + $args[] = '--no-interaction'; + } + + putenv('COMPOSER=' . $composerJsonFilePath); + $_ENV['COMPOSER'] = $composerJsonFilePath; + $_SERVER['COMPOSER'] = $composerJsonFilePath; $composerApplication = new ComposerApplication(); $composerApplication->setAutoExit(false); @@ -109,10 +96,11 @@ private function run(array $args, callable $callback): void ]); $argvInput = new ArgvInput(['composer', ...$args]); + $bufferedOutput = ''; - $output = new class($callback) extends Output { + $output = $callback instanceof OutputInterface ? $callback : new class($callback, $bufferedOutput) extends Output { /** @param callable $callback */ - public function __construct(private $callback, public string $output = '') + public function __construct(private $callback, public string &$output) { parent::__construct(); } @@ -131,25 +119,28 @@ public function doWrite(string $message, bool $newline): void $exitCode = $composerApplication->run($argvInput, $output); + putenv('COMPOSER='); + unset($_ENV['COMPOSER'], $_SERVER['COMPOSER']); + if (0 !== $exitCode) { - throw new ComposerError('The Composer process failed: ' . $output->output); + throw new ComposerError('The Composer process failed: ' . $bufferedOutput); } $this->logger->debug('Composer command was successful.', [ 'args' => implode(' ', $args), - 'output' => $output->output, + 'output' => $bufferedOutput, ]); } - private function writeInstalled(string $path): void + private function writeInstalled(string $path, string $composerFilePath): void { - file_put_contents("{$path}/composer.installed", hash('sha256', json_encode($this->configuration, \JSON_THROW_ON_ERROR))); + file_put_contents("{$path}/composer.installed", hash('sha256', json_encode(file_get_contents($composerFilePath), \JSON_THROW_ON_ERROR))); } - private function isInstalled(string $path): bool + private function isInstalled(string $path, string $composerFilePath): bool { $path = "{$path}/composer.installed"; - return file_exists($path) && file_get_contents($path) === hash('sha256', json_encode($this->configuration, \JSON_THROW_ON_ERROR)); + return file_exists($path) && file_get_contents($path) === hash('sha256', json_encode(file_get_contents($composerFilePath), \JSON_THROW_ON_ERROR)); } } diff --git a/src/Import/Remote/PackageImporter.php b/src/Import/Remote/PackageImporter.php index b62d2b7c..657435cf 100644 --- a/src/Import/Remote/PackageImporter.php +++ b/src/Import/Remote/PackageImporter.php @@ -8,7 +8,6 @@ use Castor\Import\Exception\RemoteNotAllowed; use Castor\Import\Mount; use Castor\Kernel; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Input\InputInterface; /** @internal */ @@ -16,11 +15,8 @@ class PackageImporter { public function __construct( private readonly InputInterface $input, - private readonly LoggerInterface $logger, private readonly Composer $composer, private readonly Kernel $kernel, - /** @var array */ - private array $imports = [], ) { } @@ -35,42 +31,37 @@ public function requireAutoload(): void require $autoloadPath; } - /** @phpstan-param ImportSource $source */ - public function addPackage(string $scheme, string $package, ?string $file = null, ?string $version = null, ?string $vcs = null, ?array $source = null): void + public function install(Mount $mount, bool $update = false, bool $displayProgress = true): void + { + $this->composer->install($mount->path, $update, $displayProgress); + } + + public function importFromPackage(string $scheme, string $package, ?string $file = null): void { if (!$this->allowsRemote()) { throw new RemoteNotAllowed('Remote imports are disabled.'); } - $requiredVersion = $version ?? '*'; - - if (isset($this->imports[$package]) && $this->imports[$package]->version !== $requiredVersion) { - throw new ImportError(sprintf('The package "%s" is already required in version "%s", could not require it in version "%s"', $package, $this->imports[$package]->version, $requiredVersion)); - } - if (!preg_match('#^(?[^/]+)/(?[^/]+)$#', $package)) { throw new InvalidImportFormat(sprintf('The import path must be formatted like this: "%s:///".', $scheme)); } - if ('composer' === $scheme) { - if (null !== $source) { - throw new InvalidImportFormat('The "source" argument is not supported for Composer/Packagist packages.'); + if ('composer' === $scheme || 'package' === $scheme) { + if ('package' === $scheme) { + @trigger_deprecation('castor/castor', '0.16.0', 'The "package" scheme is deprecated, use "composer" instead.'); } - $this->addPackageWithComposer($package, version: $requiredVersion, repositoryUrl: $vcs, file: $file); + $packageDirectory = PathHelper::getRoot() . Composer::VENDOR_DIR . $package; - return; - } - - if ('package' === $scheme) { - if (null !== $version || null !== $vcs) { - throw new InvalidImportFormat('The "source" and "vcs" arguments are not supported for non-Composer packages.'); - } - if (null === $source) { - throw new InvalidImportFormat('The "source" argument is required for non-Composer packages.'); + if (!file_exists($packageDirectory)) { + throw new ImportError(sprintf('The package "%s" is not installed, make sure you required it in your castor.composer.json file.', $package)); } - $this->addPackageWithComposer($package, version: 'v1', source: $source, file: $file); + $this->kernel->addMount(new Mount( + PathHelper::getRoot() . Composer::VENDOR_DIR . $package . '/' . ($file ?? ''), + allowEmptyEntrypoint: true, + allowRemotePackage: false, + )); return; } @@ -78,86 +69,12 @@ public function addPackage(string $scheme, string $package, ?string $file = null throw new InvalidImportFormat(sprintf('The import scheme "%s" is not supported.', $scheme)); } - public function fetchPackages(): bool - { - if (!$this->imports) { - return false; - } - - // Need to look for the raw options as the input is not yet parsed - $forceUpdate = true !== $this->input->getParameterOption('--update-remotes', true); - $displayProgress = 'list' !== $this->input->getFirstArgument(); - - $this->composer->update($forceUpdate, $displayProgress); - - $this->requireAutoload(); - - foreach ($this->imports as $package => $import) { - foreach ($import->getFiles() as $file) { - $this->kernel->addMount(new Mount( - PathHelper::getRoot() . Composer::VENDOR_DIR . $package . '/' . ($file ?? ''), - allowEmptyEntrypoint: true, - )); - } - } - - $this->imports = []; - - return true; - } - public function clean(): void { $this->composer->remove(); } - /** - * @param ?array{ - * url?: string, - * type?: "git" | "svn", - * reference?: string, - * } $source - */ - private function addPackageWithComposer(string $package, string $version, ?string $repositoryUrl = null, ?array $source = null, ?string $file = null): void - { - $this->logger->info('Importing remote package with Composer.', [ - 'package' => $package, - 'version' => $version, - ]); - - $json = $this->composer->getConfiguration(); - - $json['require'][$package] = $version; - - if ($repositoryUrl) { - $json['repositories'][] = [ - 'type' => 'vcs', - 'url' => $repositoryUrl, - ]; - } - - if ($source) { - if (!isset($source['url'], $source['type'], $source['reference'])) { - throw new ImportError('The "source" argument must contain "url", "type" and "reference" keys.'); - } - - $json['repositories'][] = [ - 'type' => 'package', - 'package' => [ - 'name' => $package, - 'version' => $version, - 'source' => $source, - ], - ]; - } - - $this->composer->setConfiguration($json); - - $this->imports[$package] ??= new Import($version); - $this->imports[$package]->addFile($file); - } - - private function allowsRemote(): bool + public function allowsRemote(): bool { if ($_SERVER['CASTOR_NO_REMOTE'] ?? false) { return false; @@ -167,25 +84,3 @@ private function allowsRemote(): bool return true === $this->input->getParameterOption('--no-remote', true); } } - -class Import -{ - /** @var array */ - private array $files; - - public function __construct( - public readonly string $version, - ) { - } - - public function addFile(?string $file = null): void - { - $this->files[] = $file; - } - - /** @return array */ - public function getFiles(): array - { - return array_unique($this->files); - } -} diff --git a/src/Kernel.php b/src/Kernel.php index fd8161aa..05aec6ed 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -53,23 +53,21 @@ public function boot(InputInterface $input, OutputInterface $output): void $this->eventDispatcher->dispatch(new BeforeBootEvent($this->application)); - $this->addMount(new Mount($this->rootDir)); + $allowRemotePackage = true; - $hasLoadedPackages = false; + if ($_SERVER['CASTOR_NO_REMOTE'] ?? false) { + $allowRemotePackage = false; + } elseif (true !== $input->getParameterOption('--no-remote', true)) { + $allowRemotePackage = false; + } + + $this->addMount(new Mount($this->rootDir, allowRemotePackage: $allowRemotePackage)); while ($mount = array_shift($this->mounts)) { $currentFunctions = get_defined_functions()['user']; $currentClasses = get_declared_classes(); $this->load($mount, $currentFunctions, $currentClasses, $input, $output); - - if ($this->packageImporter->fetchPackages()) { - $hasLoadedPackages = true; - } - } - - if (!$hasLoadedPackages) { - $this->packageImporter->clean(); } } @@ -89,6 +87,13 @@ private function load( InputInterface $input, OutputInterface $output ): void { + if ($mount->allowRemotePackage) { + $update = true !== $input->getParameterOption('--update-remotes', true); + $displayProgress = 'list' !== $input->getFirstArgument(); + + $this->packageImporter->install($mount, $update, $displayProgress); + } + try { $this->requireEntrypoint($mount->path); } catch (CouldNotFindEntrypointException $e) { diff --git a/src/functions.php b/src/functions.php index bf29a590..9621f56d 100644 --- a/src/functions.php +++ b/src/functions.php @@ -442,7 +442,11 @@ function http_client(): HttpClientInterface */ function import(string $path, ?string $file = null, ?string $version = null, ?string $vcs = null, ?array $source = null): void { - Container::get()->importer->import($path, $file, $version, $vcs, $source); + if (null !== $version || null !== $vcs || null !== $source) { + @trigger_deprecation('castor/castor', '0.16.0', 'The "version", "vcs" and "source" arguments are deprecated, use the `castor.composer.json` file instead.'); + } + + Container::get()->importer->import($path, $file); } function mount(string $path, ?string $namespacePrefix = null): void diff --git a/tests/Generated/ImportInvalidImportTest.php b/tests/Generated/CastorComposerTest.php similarity index 52% rename from tests/Generated/ImportInvalidImportTest.php rename to tests/Generated/CastorComposerTest.php index f306634e..5d5a68e9 100644 --- a/tests/Generated/ImportInvalidImportTest.php +++ b/tests/Generated/CastorComposerTest.php @@ -5,18 +5,18 @@ use Castor\Tests\TaskTestCase; use Symfony\Component\Process\Exception\ProcessFailedException; -class ImportInvalidImportTest extends TaskTestCase +class CastorComposerTest extends TaskTestCase { - // no task + // castor:composer public function test(): void { - $process = $this->runTask([], '{{ base }}/tests/fixtures/broken/import-invalid-import', needRemote: true); + $process = $this->runTask(['castor:composer']); - if (1 !== $process->getExitCode()) { + if (0 !== $process->getExitCode()) { throw new ProcessFailedException($process); } $this->assertStringEqualsFile(__FILE__ . '.output.txt', $process->getOutput()); - $this->assertStringEqualsFile(__FILE__ . '.err.txt', $process->getErrorOutput()); + $this->assertSame('', $process->getErrorOutput()); } } diff --git a/tests/Generated/CastorComposerTest.php.output.txt b/tests/Generated/CastorComposerTest.php.output.txt new file mode 100644 index 00000000..18e2569a --- /dev/null +++ b/tests/Generated/CastorComposerTest.php.output.txt @@ -0,0 +1,58 @@ + ______ + / ____/___ ____ ___ ____ ____ ________ _____ + / / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/ +/ /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / +\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/ + /_/ +Composer version 2.7.2 2024-03-11 17:12:18 + +Usage: + command [options] [arguments] + +Options: + -h, --help Display help for the given command. When no command is given display help for the list command + -q, --quiet Do not output any message + -V, --version Display this application version + --ansi|--no-ansi Force (or disable --no-ansi) ANSI output + -n, --no-interaction Do not ask any interactive question + --profile Display timing and memory usage information + --no-plugins Whether to disable plugins. + --no-scripts Skips the execution of all scripts defined in composer.json file. + -d, --working-dir=WORKING-DIR If specified, use the given directory as working directory. + --no-cache Prevent use of the cache + -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + +Available commands: + about Shows a short information about Composer + archive Creates an archive of this composer package + audit Checks for security vulnerability advisories for installed packages + browse [home] Opens the package's repository URL or homepage in your browser + bump Increases the lower limit of your composer.json requirements to the currently installed versions + check-platform-reqs Check that platform requirements are satisfied + clear-cache [clearcache|cc] Clears composer's internal package cache + completion Dump the shell completion script + config Sets config options + create-project Creates new project from a package into given directory + depends [why] Shows which packages cause the given package to be installed + diagnose Diagnoses the system to identify common errors + dump-autoload [dumpautoload] Dumps the autoloader + exec Executes a vendored binary/script + fund Discover how to help fund the maintenance of your dependencies + global Allows running commands in the global composer dir ($COMPOSER_HOME) + help Display help for a command + init Creates a basic composer.json file in current directory + install [i] Installs the project dependencies from the composer.lock file if present, or falls back on the composer.json + licenses Shows information about licenses of dependencies + list List commands + outdated Shows a list of installed packages that have updates available, including their latest version + prohibits [why-not] Shows which packages prevent the given package from being installed + reinstall Uninstalls and reinstalls the given package names + remove [rm] Removes a package from the require or require-dev + require [r] Adds required packages to your composer.json and installs them + run-script [run] Runs the scripts defined in composer.json + search Searches for packages + show [info] Shows information about packages + status Shows a list of locally modified packages + suggests Shows package suggestions + update [u|upgrade] Updates your dependencies to the latest version according to composer.json, and updates the composer.lock file + validate Validates a composer.json and composer.lock diff --git a/tests/Generated/ImportComposerSourceTest.php b/tests/Generated/ImportComposerNotExistingTest.php similarity index 82% rename from tests/Generated/ImportComposerSourceTest.php rename to tests/Generated/ImportComposerNotExistingTest.php index f13ea997..2171cf25 100644 --- a/tests/Generated/ImportComposerSourceTest.php +++ b/tests/Generated/ImportComposerNotExistingTest.php @@ -5,12 +5,12 @@ use Castor\Tests\TaskTestCase; use Symfony\Component\Process\Exception\ProcessFailedException; -class ImportComposerSourceTest extends TaskTestCase +class ImportComposerNotExistingTest extends TaskTestCase { // no task public function test(): void { - $process = $this->runTask([], '{{ base }}/tests/fixtures/broken/import-composer-source', needRemote: true); + $process = $this->runTask([], '{{ base }}/tests/fixtures/broken/import-composer-not-existing', needRemote: true); if (1 !== $process->getExitCode()) { throw new ProcessFailedException($process); diff --git a/tests/Generated/ImportComposerNotExistingTest.php.err.txt b/tests/Generated/ImportComposerNotExistingTest.php.err.txt new file mode 100644 index 00000000..be4b26f2 --- /dev/null +++ b/tests/Generated/ImportComposerNotExistingTest.php.err.txt @@ -0,0 +1,10 @@ +In castor.php line 5: + + Could not import "foo/bar" in ".../tests/fixtures/broken/import-composer-not-existing/castor.php" on line 5. Reason: The package "foo/bar" is not installed, make sure you required it in your castor.composer.json file. + + +In PackageImporter.php line XXXX: + + The package "foo/bar" is not installed, make sure you required it in your castor.composer.json file. + + diff --git a/tests/Generated/ImportComposerSourceTest.php.output.txt b/tests/Generated/ImportComposerNotExistingTest.php.output.txt similarity index 100% rename from tests/Generated/ImportComposerSourceTest.php.output.txt rename to tests/Generated/ImportComposerNotExistingTest.php.output.txt diff --git a/tests/Generated/ImportComposerSourceTest.php.err.txt b/tests/Generated/ImportComposerSourceTest.php.err.txt deleted file mode 100644 index e98a9dca..00000000 --- a/tests/Generated/ImportComposerSourceTest.php.err.txt +++ /dev/null @@ -1,10 +0,0 @@ -In castor.php line 5: - - Could not import "foo/bar" in ".../tests/fixtures/broken/import-composer-source/castor.php" on line 5. Reason: The "source" argument is not supported for Composer/Packagist packages. - - -In PackageImporter.php line XXXX: - - The "source" argument is not supported for Composer/Packagist packages. - - diff --git a/tests/Generated/ImportInvalidImportTest.php.err.txt b/tests/Generated/ImportInvalidImportTest.php.err.txt deleted file mode 100644 index 63fa9f8d..00000000 --- a/tests/Generated/ImportInvalidImportTest.php.err.txt +++ /dev/null @@ -1,5 +0,0 @@ -In castor.php line 5: - - Could not import "path-to-castor.php" in ".../tests/fixtures/broken/import-invalid-import/castor.php" on line 5. Reason: The "file", "version", "vcs" and "source" arguments can only be used with a remote import. - - diff --git a/tests/Generated/ImportInvalidImportTest.php.output.txt b/tests/Generated/ImportInvalidImportTest.php.output.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/Generated/ImportInvalidPackageTest.php.err.txt b/tests/Generated/ImportInvalidPackageTest.php.err.txt index 1731a71e..e378ba90 100644 --- a/tests/Generated/ImportInvalidPackageTest.php.err.txt +++ b/tests/Generated/ImportInvalidPackageTest.php.err.txt @@ -1,10 +1,10 @@ In castor.php line 5: - Could not import "foo/bar" in ".../tests/fixtures/broken/import-invalid-package/castor.php" on line 5. Reason: The "source" argument must contain "url", "type" and "reference" keys. + Could not import "foo/bar" in ".../tests/fixtures/broken/import-invalid-package/castor.php" on line 5. Reason: The package "foo/bar" is not installed, make sure you required it in your castor.composer.json file. In PackageImporter.php line XXXX: - The "source" argument must contain "url", "type" and "reference" keys. + The package "foo/bar" is not installed, make sure you required it in your castor.composer.json file. diff --git a/tests/Generated/ImportInvalidPackageTest.php.output.txt b/tests/Generated/ImportInvalidPackageTest.php.output.txt index e69de29b..717d8955 100644 --- a/tests/Generated/ImportInvalidPackageTest.php.output.txt +++ b/tests/Generated/ImportInvalidPackageTest.php.output.txt @@ -0,0 +1 @@ +hh:mm:ss WARNING [castor] User Deprecated: Since castor/castor 0.16.0: The "package" scheme is deprecated, use "composer" instead. ["exception" => ErrorException { …}] diff --git a/tests/Generated/ImportSamePackageDifferentVersionTest.php b/tests/Generated/ImportSamePackageDifferentVersionTest.php deleted file mode 100644 index ca8f82cb..00000000 --- a/tests/Generated/ImportSamePackageDifferentVersionTest.php +++ /dev/null @@ -1,22 +0,0 @@ -runTask([], '{{ base }}/tests/fixtures/broken/import-same-package-different-version', needRemote: true); - - if (1 !== $process->getExitCode()) { - throw new ProcessFailedException($process); - } - - $this->assertStringEqualsFile(__FILE__ . '.output.txt', $process->getOutput()); - $this->assertStringEqualsFile(__FILE__ . '.err.txt', $process->getErrorOutput()); - } -} diff --git a/tests/Generated/ImportSamePackageDifferentVersionTest.php.err.txt b/tests/Generated/ImportSamePackageDifferentVersionTest.php.err.txt deleted file mode 100644 index 27d9cbaf..00000000 --- a/tests/Generated/ImportSamePackageDifferentVersionTest.php.err.txt +++ /dev/null @@ -1,10 +0,0 @@ -In castor.php line 6: - - Could not import "pyrech/castor-example" in ".../tests/fixtures/broken/import-same-package-different-version/castor.php" on line 6. Reason: The package "pyrech/castor-example" is already required in version "^1.0", could not require it in version "^2.0" - - -In PackageImporter.php line XXXX: - - The package "pyrech/castor-example" is already required in version "^1.0", could not require it in version "^2.0" - - diff --git a/tests/Generated/ImportSamePackageDifferentVersionTest.php.output.txt b/tests/Generated/ImportSamePackageDifferentVersionTest.php.output.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/Generated/ImportSamePackageWithDefaultVersionTest.php.output.txt b/tests/Generated/ImportSamePackageWithDefaultVersionTest.php.output.txt index d4cc72d3..8b121264 100644 --- a/tests/Generated/ImportSamePackageWithDefaultVersionTest.php.output.txt +++ b/tests/Generated/ImportSamePackageWithDefaultVersionTest.php.output.txt @@ -20,5 +20,7 @@ Available commands: completion Dump the shell completion script help Display help for a command list List commands + castor + castor:composer [composer] Interact with built-in Composer for castor pyrech pyrech:hello-example Hello from example! diff --git a/tests/Generated/LayoutWithFolderTest.php.output.txt b/tests/Generated/LayoutWithFolderTest.php.output.txt index 7bd56527..426ae197 100644 --- a/tests/Generated/LayoutWithFolderTest.php.output.txt +++ b/tests/Generated/LayoutWithFolderTest.php.output.txt @@ -14,7 +14,9 @@ Options: -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: - completion Dump the shell completion script + completion Dump the shell completion script hello - help Display help for a command - list List commands + help Display help for a command + list List commands + castor + castor:composer [composer] Interact with built-in Composer for castor diff --git a/tests/Generated/LayoutWithOldFolderTest.php.output.txt b/tests/Generated/LayoutWithOldFolderTest.php.output.txt index 785f67bc..2f19e5df 100644 --- a/tests/Generated/LayoutWithOldFolderTest.php.output.txt +++ b/tests/Generated/LayoutWithOldFolderTest.php.output.txt @@ -15,7 +15,9 @@ Options: -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: - completion Dump the shell completion script + completion Dump the shell completion script hello - help Display help for a command - list List commands + help Display help for a command + list List commands + castor + castor:composer [composer] Interact with built-in Composer for castor diff --git a/tests/Generated/ListTest.php.output.txt b/tests/Generated/ListTest.php.output.txt index 0f0822a3..4f8c5a0d 100644 --- a/tests/Generated/ListTest.php.output.txt +++ b/tests/Generated/ListTest.php.output.txt @@ -11,6 +11,7 @@ args:passthru Dumps all argu bar:bar Prints bar, but also executes foo cache:complex Cache with usage of CacheItemInterface cache:simple Cache a simple call +castor:composer Interact with built-in Composer for castor castor:phar:build Build phar for all systems castor:phar:darwin Build phar for MacOS system castor:phar:install install dependencies diff --git a/tests/fixtures/broken/import-composer-not-existing/castor.php b/tests/fixtures/broken/import-composer-not-existing/castor.php new file mode 100644 index 00000000..f743282f --- /dev/null +++ b/tests/fixtures/broken/import-composer-not-existing/castor.php @@ -0,0 +1,5 @@ + 'git']); diff --git a/tests/fixtures/broken/import-invalid-format/castor.php b/tests/fixtures/broken/import-invalid-format/castor.php index fc11b93c..bed39016 100644 --- a/tests/fixtures/broken/import-invalid-format/castor.php +++ b/tests/fixtures/broken/import-invalid-format/castor.php @@ -2,4 +2,4 @@ use function Castor\import; -import('composer://invalid-package-name', version: '^1.0'); +import('composer://invalid-package-name'); diff --git a/tests/fixtures/broken/import-invalid-import/castor.php b/tests/fixtures/broken/import-invalid-import/castor.php deleted file mode 100644 index 1aa7d823..00000000 --- a/tests/fixtures/broken/import-invalid-import/castor.php +++ /dev/null @@ -1,5 +0,0 @@ - 'git']); +import('package://foo/bar'); diff --git a/tests/fixtures/broken/import-same-package-different-version/castor.php b/tests/fixtures/broken/import-same-package-different-version/castor.php deleted file mode 100644 index 62464849..00000000 --- a/tests/fixtures/broken/import-same-package-different-version/castor.php +++ /dev/null @@ -1,6 +0,0 @@ -