Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add system to import functions from remote #136

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,7 @@
## Not released yet

* Add `fingerprint()` function to condition code execution based on some hash changes
* Allow to import functions from remote resources
* Better handle default Symfony commands when no castor file exists yet

## 0.8.0 (2023-08-16)
Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -125,6 +125,7 @@ Discover more by reading the docs:
* [Handling signals](doc/12-signals.md)
* [Repacking your application in a new phar](doc/13-repack.md)
* [Fingerprinting and code execution when a hash changes](doc/14-fingerprint.md)
* [Import remote functions](doc/15-remote.md)

## Questions and answers

Expand Down
19 changes: 15 additions & 4 deletions bin/generate-tests.php
Expand Up @@ -14,7 +14,7 @@
$application = ApplicationFactory::create();
$application->setAutoExit(false);
$application
->run(new ArrayInput(['command' => 'list', '--format' => 'json']), $o = new BufferedOutput())
->run(new ArrayInput(['command' => 'list', '--format' => 'json', '--no-trust']), $o = new BufferedOutput())
;
$applicationDescription = json_decode($o->fetch(), true);

Expand Down Expand Up @@ -50,8 +50,12 @@
'run:run-parallel',
'fingerprint:task-with-some-fingerprint', // Tested in Castor\Tests\Fingerprint\FingerprintTaskWithSomeFingerprintTest
'fingerprint:task-with-some-fingerprint-with-helper', // Tested in Castor\Tests\Fingerprint\FingerprintTaskWithSomeFingerprintWithHelperTest
'import:hello',
// Imported tasks
'pyrech:hello',
'pyrech:hello-world',
];
$optionFilterList = array_flip(['help', 'quiet', 'verbose', 'version', 'ansi', 'no-ansi', 'no-interaction', 'context']);
$optionFilterList = array_flip(['help', 'quiet', 'verbose', 'version', 'ansi', 'no-ansi', 'no-interaction', 'context', 'trust', 'no-trust']);
foreach ($applicationDescription['commands'] as $command) {
if (in_array($command['name'], $commandFilterList, true)) {
continue;
Expand Down Expand Up @@ -88,7 +92,7 @@

add_test(['parallel:sleep', '--sleep5', '0', '--sleep7', '0', '--sleep10', '0'], 'ParallelSleepTest');
add_test(['context:context', '--context', 'run'], 'ContextContextRunTest');
add_test(['context:context', '--context', 'my_default', '-vvv'], 'ContextContextMyDefaultTest');
add_test(['context:context', '--context', 'my_default', '-v'], 'ContextContextMyDefaultTest');
add_test(['context:context', '--context', 'no_no_exist'], 'ContextContextDoNotExistTest');
add_test(['context:context', '--context', 'production'], 'ContextContextProductionTest');
add_test(['context:context', '--context', 'path'], 'ContextContextPathTest');
Expand All @@ -99,13 +103,20 @@
add_test(['unknown:command'], 'NoConfigUnknownTest', '/tmp');
add_test(['unknown:command', 'toto', '--foo', 1], 'NoConfigUnknownWithArgsTest', '/tmp');
add_test(['completion', 'bash'], 'NoConfigCompletionTest', '/tmp');
add_test(['import:hello'], 'ImportHelloAskingTrust', noTrust: false);
add_test(['import:hello', '--no-trust'], 'ImportHelloNoTrustForced', noTrust: false);
add_test(['import:hello', '--trust'], 'ImportHelloTrustForce', noTrust: false);

function add_test(array $args, string $class, string $cwd = null)
function add_test(array $args, string $class, string $cwd = null, bool $noTrust = true)
{
$fp = fopen(__FILE__, 'r');
fseek($fp, __COMPILER_HALT_OFFSET__ + 1);
$template = stream_get_contents($fp);

if ($noTrust) {
$args[] = '--no-trust';
}

$process = new Process(
[\PHP_BINARY, __DIR__ . '/castor', ...$args],
cwd: $cwd ?: __DIR__ . '/../',
Expand Down
4 changes: 4 additions & 0 deletions doc/02-basic-usage.md
Expand Up @@ -67,6 +67,10 @@ import(__DIR__ . '/my-app/castor');
> You cannot dynamically import commands. The `import()` function must be called
> at the top level of the file.

> **Note**
> You can also import functions from a remote resource. See the
> [related documentation](15-remote.md).

## Overriding command name, namespace or description

The `Castor\Attribute\AsTask` attribute takes three optional
Expand Down
2 changes: 1 addition & 1 deletion doc/06-helper.md
Expand Up @@ -197,7 +197,7 @@ function foo()
}
```

By default it caches items on the filesystem, in the `/tmp/castor` directory.
By default it caches items on the filesystem, in the `$HOME/.castor/cache` directory.
The function also prefix the key with a hash of the project directory to avoid
any collision between different project.

Expand Down
58 changes: 58 additions & 0 deletions doc/15-remote.md
@@ -0,0 +1,58 @@
# Import remote functions

Castor can import functions from your filesystem but also from a remote resource.

## Importing functions

When importing functions from a remote resource, Castor will download the files
and store them in `$HOME/.castor/remote/`.

### From a GitHub repository

To import functions from a GitHub repository, pass a path to the `import()`
function, formatted like this:

```
github://<user>/<repository>/<path of the php file to import>@<version>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should document what version could be. A branch? A tag? A commit hash?

```

Here is an example:

```php
use function Castor\import;

import('github://pyrech/castor-setup-php/castor.php@main');

#[AsTask()]
function hello(): void
{
\pyrech\helloWorld();
}
```

> **Note**
> If path of the file is empty, it will import the `castor.php` file at the root
> of the repository.

## Trusting remote resource

For security reasons, each time your Castor project tries to import a remote
resource, Castor will warn you to ask if you trust it.

For each remote resource, Castor will ask you what to do. You can either:
- `not now`: Castor will **not import** the function but will **ask you again**
next time ;
- `never`: Castor will **not import** the function and will persist your choice
to **not ask you again** in the future ;
- `only this time`: Castor will **import** the function but will ask you again the
next time ;
- `always`: Castor will **import** the function and will persist your choice
to **not ask you again** in the future.

You can also pass the `--trust` (or `--no-trust`) options to automatically trust
(or not) **all** remote resources without being asked for.

> **Warning**
> The `--trust` option should be used with caution as it could lead to malicious
> code execution. The main use case of this option is to be used in a Continuous
> Integration environment.
15 changes: 15 additions & 0 deletions examples/import.php
@@ -0,0 +1,15 @@
<?php

namespace import;

use Castor\Attribute\AsTask;

use function Castor\import;

import('github://pyrech/castor-setup-php/castor.php@main');

#[AsTask(description: 'Use a function imported from a remote repository')]
function hello(): void
{
\pyrech\helloWorld();
}
5 changes: 5 additions & 0 deletions phpstan-baseline.neon
Expand Up @@ -5,6 +5,11 @@ parameters:
count: 1
path: examples/args.php

-
message: "#^Function pyrech\\\\helloWorld not found\\.$#"
count: 1
path: examples/import.php

-
message: "#^Default value of the parameter \\#1 \\$data \\(array\\{\\}\\) of method Castor\\\\Context\\:\\:__construct\\(\\) is incompatible with type array\\{name\\: string, production\\: bool, foo\\?\\: string\\}\\.$#"
count: 1
Expand Down
30 changes: 23 additions & 7 deletions src/Console/Application.php
Expand Up @@ -11,14 +11,15 @@
use Castor\GlobalHelper;
use Castor\Monolog\Processor\ProcessProcessor;
use Castor\SectionOutput;
use Castor\PlatformUtil;
use Castor\Stub\StubsGenerator;
use Castor\TaskDescriptor;
use Castor\VerbosityLevel;
use Joli\JoliNotif\Util\OsHelper;
use Monolog\Logger;
use Symfony\Bridge\Monolog\Handler\ConsoleHandler;
use Symfony\Component\Console\Application as SymfonyApplication;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\CompleteCommand;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
Expand Down Expand Up @@ -88,7 +89,12 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
{
GlobalHelper::setCommand($command);

if ('_complete' !== $command->getName() && !class_exists(\RepackedApplication::class)) {
if ($command instanceof TaskCommand) {
$context = $this->createContext($input, $output);
GlobalHelper::setInitialContext($context);
}

if (!$command instanceof CompleteCommand && !class_exists(\RepackedApplication::class)) {
$this->stubsGenerator->generateStubsIfNeeded($this->rootDir . '/.castor.stub.php');
$this->displayUpdateWarningIfNeeded(new SymfonyStyle($input, $output));
}
Expand All @@ -105,6 +111,16 @@ private function initializeApplication(): array
if (class_exists(\RepackedApplication::class)) {
$functionsRootDir = \RepackedApplication::ROOT_DIR;
}

$this->getDefinition()->addOption(
new InputOption(
'trust',
null,
InputOption::VALUE_NEGATABLE,
'Trust all the imported functions from remote resources'
)
);

// Find all potential commands / context
$functions = $this->functionFinder->findFunctions($functionsRootDir);
$tasks = [];
Expand Down Expand Up @@ -142,7 +158,7 @@ private function createContext(InputInterface $input, OutputInterface $output):
// context and it will fail later anyway
}

// occurs when running `castor -h`, or if no context is defined
// Occurs when running a native command (like `castor -h`, `castor list`, etc), or if no context is defined
if (!$input->hasOption('context')) {
return new Context();
}
Expand Down Expand Up @@ -189,9 +205,9 @@ private function displayUpdateWarningIfNeeded(SymfonyStyle $symfonyStyle): void

if ($pharPath = \Phar::running(false)) {
$assets = match (true) {
OsHelper::isWindows() || OsHelper::isWindowsSubsystemForLinux() => array_filter($latestVersion['assets'], fn (array $asset) => str_contains($asset['name'], 'windows')),
OsHelper::isMacOS() => array_filter($latestVersion['assets'], fn (array $asset) => str_contains($asset['name'], 'darwin')),
OsHelper::isUnix() => array_filter($latestVersion['assets'], fn (array $asset) => str_contains($asset['name'], 'linux')),
PlatformUtil::isWindows() || PlatformUtil::isWindowsSubsystemForLinux() => array_filter($latestVersion['assets'], fn (array $asset) => str_contains($asset['name'], 'windows')),
PlatformUtil::isMacOS() => array_filter($latestVersion['assets'], fn (array $asset) => str_contains($asset['name'], 'darwin')),
PlatformUtil::isUnix() => array_filter($latestVersion['assets'], fn (array $asset) => str_contains($asset['name'], 'linux')),
default => [],
};

Expand All @@ -209,7 +225,7 @@ private function displayUpdateWarningIfNeeded(SymfonyStyle $symfonyStyle): void
return;
}

if (OsHelper::isUnix()) {
if (PlatformUtil::isUnix()) {
$symfonyStyle->block('Run the following command to update Castor:');
$symfonyStyle->block(sprintf('<comment>curl "%s" -Lso castor && chmod u+x castor && mv castor %s</comment>', $latestReleaseUrl, $pharPath), escape: false);
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/GlobalHelper.php
Expand Up @@ -176,9 +176,9 @@ public static function setupDefaultCache(): void
{
if (!isset(self::$cache)) {
$home = PlatformUtil::getUserDirectory();
$directory = $home ? $home . '/.cache' : sys_get_temp_dir();
$directory = ($home ? $home . '/.cache' : sys_get_temp_dir()) . '/castor';

self::setCache(new FilesystemAdapter(directory: $directory . '/castor'));
self::setCache(new FilesystemAdapter(directory: $directory));
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/Remote/Exception/ImportError.php
@@ -0,0 +1,7 @@
<?php

namespace Castor\Remote\Exception;

class ImportError extends \RuntimeException
{
}
7 changes: 7 additions & 0 deletions src/Remote/Exception/InvalidImportUrl.php
@@ -0,0 +1,7 @@
<?php

namespace Castor\Remote\Exception;

class InvalidImportUrl extends \RuntimeException
{
}
12 changes: 12 additions & 0 deletions src/Remote/Exception/NotTrusted.php
@@ -0,0 +1,12 @@
<?php

namespace Castor\Remote\Exception;

class NotTrusted extends \RuntimeException
{
public function __construct(
public readonly string $url,
) {
parent::__construct("The remote resource {$url} is not trusted.");
}
}