Skip to content

Commit

Permalink
Add system to import functions from remote
Browse files Browse the repository at this point in the history
  • Loading branch information
pyrech committed Oct 6, 2023
1 parent 2cd26b1 commit 0817938
Show file tree
Hide file tree
Showing 62 changed files with 534 additions and 57 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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>
```

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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Castor\Remote\Exception;

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

namespace Castor\Remote\Exception;

class InvalidImportUrl extends \RuntimeException
{
}
12 changes: 12 additions & 0 deletions src/Remote/Exception/NotTrusted.php
Original file line number Diff line number Diff line change
@@ -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.");
}
}

0 comments on commit 0817938

Please sign in to comment.