diff --git a/bin/generate-tests.php b/bin/generate-tests.php index 6a992ae5..04cd7e9a 100755 --- a/bin/generate-tests.php +++ b/bin/generate-tests.php @@ -86,6 +86,7 @@ 'qa:phpstan:update', // Customized tests 'fingerprint:task-with-a-fingerprint-and-force', + 'fingerprint:task-with-a-fingerprint-global', 'fingerprint:task-with-a-fingerprint', 'fingerprint:task-with-complete-fingerprint-check', 'log:all-level', diff --git a/doc/going-further/helpers/fingerprint.md b/doc/going-further/helpers/fingerprint.md index 0780ad68..45c57892 100644 --- a/doc/going-further/helpers/fingerprint.md +++ b/doc/going-further/helpers/fingerprint.md @@ -32,6 +32,10 @@ function task_with_a_fingerprint(): void > You can use the `$force` parameter of the `fingerprint()` function to force > the execution of the callback even if the fingerprint has not changed. +> [!NOTE] +> By default the fingerprint is scoped to the current project, but you can use +> the `$global` parameter to make it shared across all projects. + ## The `hasher()` function Most of the time, you will want your fingerprint hash to be based on the content diff --git a/examples/fingerprint.php b/examples/fingerprint.php index 20bef415..99ac8359 100644 --- a/examples/fingerprint.php +++ b/examples/fingerprint.php @@ -29,6 +29,23 @@ function task_with_a_fingerprint(): void io()->writeln('Cool! I finished!'); } +#[AsTask(description: 'Execute a callback only if the global fingerprint has changed (Shared across all projects)')] +function task_with_a_fingerprint_global(): void +{ + io()->writeln('Hello Task with Global Fingerprint!'); + + fingerprint( + callback: function () { + io()->writeln('Cool, no global fingerprint! Executing...'); + }, + id: 'my_global_fingerprint_check', + fingerprint: my_fingerprint_check(), + global: true, // This ensures that the fingerprint is shared across all projects (not only the current one) + ); + + io()->writeln('Cool! I finished global!'); +} + #[AsTask(description: 'Check if the fingerprint has changed before executing some code')] function task_with_complete_fingerprint_check(): void { diff --git a/src/Fingerprint/FingerprintHelper.php b/src/Fingerprint/FingerprintHelper.php index 42b052fc..863afab1 100644 --- a/src/Fingerprint/FingerprintHelper.php +++ b/src/Fingerprint/FingerprintHelper.php @@ -2,6 +2,7 @@ namespace Castor\Fingerprint; +use Castor\Helper\PathHelper; use Psr\Cache\CacheItemPoolInterface; use Symfony\Contracts\Cache\CacheInterface; @@ -15,9 +16,9 @@ public function __construct( ) { } - public function verifyFingerprintFromHash(string $id, string $fingerprint): bool + public function verifyFingerprintFromHash(string $id, string $fingerprint, bool $global = false): bool { - $itemKey = $id . self::SUFFIX; + $itemKey = $this->getItemKey($id, $global); if (false === $this->cache->hasItem($itemKey)) { return false; @@ -36,9 +37,9 @@ public function verifyFingerprintFromHash(string $id, string $fingerprint): bool return false; } - public function postProcessFingerprintForHash(string $id, string $hash): void + public function postProcessFingerprintForHash(string $id, string $hash, bool $global = false): void { - $itemKey = $id . self::SUFFIX; + $itemKey = $this->getItemKey($id, $global); $cacheItem = $this->cache->getItem($itemKey); @@ -47,4 +48,17 @@ public function postProcessFingerprintForHash(string $id, string $hash): void $cacheItem->expiresAt(new \DateTimeImmutable('+1 month')); $this->cache->save($cacheItem); } + + private function getItemKey(string $id, bool $global = false): string + { + if ($global) { + return $id . self::SUFFIX; + } + + return \sprintf( + '%s-%s', + hash('xxh128', PathHelper::getRoot()), + $id, + ); + } } diff --git a/src/Helper/HasherHelper.php b/src/Helper/HasherHelper.php index cf6e5dd9..157744ab 100644 --- a/src/Helper/HasherHelper.php +++ b/src/Helper/HasherHelper.php @@ -43,6 +43,12 @@ public function writeFile(string $path, FileHashStrategy $strategy = FileHashStr $path = getcwd() . '/' . $path; } + $path = realpath($path); + + if (false === $path) { + throw new \InvalidArgumentException(\sprintf('The path "%s" is not a valid path.', $path)); + } + if (!is_file($path)) { throw new \InvalidArgumentException(\sprintf('The path "%s" is not a file.', $path)); } @@ -79,11 +85,11 @@ public function writeWithFinder(Finder $finder, FileHashStrategy $strategy = Fil foreach ($finder as $file) { switch ($strategy) { case FileHashStrategy::Content: - hash_update_file($this->hashContext, $file->getPathname()); + hash_update_file($this->hashContext, $file->getRealPath()); break; case FileHashStrategy::MTimes: - hash_update($this->hashContext, "{$file->getPathname()}:{$file->getMTime()}"); + hash_update($this->hashContext, "{$file->getRealPath()}:{$file->getMTime()}"); break; } @@ -105,6 +111,9 @@ public function writeGlob(string $pattern, FileHashStrategy $strategy = FileHash throw new \InvalidArgumentException(\sprintf('The pattern "%s" is invalid.', $pattern)); } + $files = array_map('realpath', $files); + $files = array_filter($files); + foreach ($files as $file) { switch ($strategy) { case FileHashStrategy::Content: diff --git a/src/functions.php b/src/functions.php index 52d32468..5a5a90b8 100644 --- a/src/functions.php +++ b/src/functions.php @@ -591,7 +591,7 @@ function hasher(string $algo = 'xxh128'): HasherHelper ); } -function fingerprint_exists(string $id, ?string $fingerprint = null): bool +function fingerprint_exists(string $id, ?string $fingerprint = null, bool $global = false): bool { if (null === $fingerprint) { trigger_deprecation('castor/castor', '0.18.0', 'since 0.18 fingerprint functions require an id argument.'); @@ -599,10 +599,10 @@ function fingerprint_exists(string $id, ?string $fingerprint = null): bool $fingerprint = $id; } - return Container::get()->fingerprintHelper->verifyFingerprintFromHash($id, $fingerprint); + return Container::get()->fingerprintHelper->verifyFingerprintFromHash($id, $fingerprint, $global); } -function fingerprint_save(string $id, ?string $fingerprint = null): void +function fingerprint_save(string $id, ?string $fingerprint = null, bool $global = false): void { if (null === $fingerprint) { trigger_deprecation('castor/castor', '0.18.0', 'since 0.18 fingerprint functions require an id argument.'); @@ -610,7 +610,7 @@ function fingerprint_save(string $id, ?string $fingerprint = null): void $fingerprint = $id; } - Container::get()->fingerprintHelper->postProcessFingerprintForHash($id, $fingerprint); + Container::get()->fingerprintHelper->postProcessFingerprintForHash($id, $fingerprint, $global); } // function fingerprint(callable $callback, string $fingerprint, bool $force = false): bool @@ -618,7 +618,7 @@ function fingerprint_save(string $id, ?string $fingerprint = null): void * @param string $id * @param string $fingerprint */ -function fingerprint(callable $callback, /* string */ $id = null, /* string */ $fingerprint = null, bool $force = false): bool +function fingerprint(callable $callback, /* string */ $id = null, /* string */ $fingerprint = null, bool $force = false, bool $global = false): bool { // Could only occur due du BC layer if (null === $fingerprint && null === $id) { @@ -642,9 +642,9 @@ function fingerprint(callable $callback, /* string */ $id = null, /* string */ $ $id = $fingerprint; } - if ($force || !fingerprint_exists($id, $fingerprint)) { + if ($force || !fingerprint_exists($id, $fingerprint, $global)) { $callback(); - fingerprint_save($id, $fingerprint); + fingerprint_save($id, $fingerprint, $global); return true; } diff --git a/tests/Examples/Fingerprint/FingerprintTaskWithAFingerprintTest.php.output_runnable.txt b/tests/Examples/Fingerprint/FingerprintTaskWithAFingerprintTest.php.output_runnable.txt deleted file mode 100644 index 00f0ed61..00000000 --- a/tests/Examples/Fingerprint/FingerprintTaskWithAFingerprintTest.php.output_runnable.txt +++ /dev/null @@ -1,3 +0,0 @@ -Hello Task with Fingerprint! -Cool, no fingerprint! Executing... -Cool! I finished! diff --git a/tests/Examples/Fingerprint/FingerprintTaskWithAGlobalFingerprintTest.php b/tests/Examples/Fingerprint/FingerprintTaskWithAGlobalFingerprintTest.php new file mode 100644 index 00000000..866da0bd --- /dev/null +++ b/tests/Examples/Fingerprint/FingerprintTaskWithAGlobalFingerprintTest.php @@ -0,0 +1,37 @@ +runProcessAndExpect(__FILE__ . '.output_runnable.txt'); + + // should not run because the fingerprint is the same + $this->runProcessAndExpect(__FILE__ . '.output_not_runnable.txt'); + + // change file content, should run + $this->runProcessAndExpect(__FILE__ . '.output_runnable.txt', 'Hello World'); + } + + private function runProcessAndExpect(string $expectedOutputFilePath, string $withFileContent = 'Hello'): void + { + $filepath = \dirname(__DIR__, 3) . '/examples/fingerprint_file.fingerprint_single'; + if (file_exists($filepath)) { + unlink($filepath); + } + + file_put_contents($filepath, $withFileContent); + + $process = $this->runTask(['fingerprint:task-with-a-fingerprint-global']); + + if (file_exists($expectedOutputFilePath)) { + $this->assertStringEqualsFile($expectedOutputFilePath, $process->getOutput()); + } + + $this->assertSame(0, $process->getExitCode()); + } +} diff --git a/tests/Examples/Fingerprint/FingerprintTaskWithAGlobalFingerprintTest.php.output_not_runnable.txt b/tests/Examples/Fingerprint/FingerprintTaskWithAGlobalFingerprintTest.php.output_not_runnable.txt new file mode 100644 index 00000000..08db4a60 --- /dev/null +++ b/tests/Examples/Fingerprint/FingerprintTaskWithAGlobalFingerprintTest.php.output_not_runnable.txt @@ -0,0 +1,2 @@ +Hello Task with Global Fingerprint! +Cool! I finished global! diff --git a/tests/Examples/Fingerprint/FingerprintTaskWithAGlobalFingerprintTest.php.output_runnable.txt b/tests/Examples/Fingerprint/FingerprintTaskWithAGlobalFingerprintTest.php.output_runnable.txt new file mode 100644 index 00000000..04b78641 --- /dev/null +++ b/tests/Examples/Fingerprint/FingerprintTaskWithAGlobalFingerprintTest.php.output_runnable.txt @@ -0,0 +1,3 @@ +Hello Task with Global Fingerprint! +Cool, no global fingerprint! Executing... +Cool! I finished global! diff --git a/tests/Generated/ListTest.php.output.txt b/tests/Generated/ListTest.php.output.txt index e1c26723..4c7730b3 100644 --- a/tests/Generated/ListTest.php.output.txt +++ b/tests/Generated/ListTest.php.output.txt @@ -37,6 +37,7 @@ filesystem:filesystem Performs some filesystem:find Search files and directories on the filesystem fingerprint:task-with-a-fingerprint Execute a callback only if the fingerprint has changed fingerprint:task-with-a-fingerprint-and-force Check if the fingerprint has changed before executing a callback (with force option) +fingerprint:task-with-a-fingerprint-global Execute a callback only if the global fingerprint has changed (Shared across all projects) fingerprint:task-with-complete-fingerprint-check Check if the fingerprint has changed before executing some code foo:bar Echo foo bar foo:foo Prints foo