|
9 | 9 | use Castor\Helper\PathHelper; |
10 | 10 | use Symfony\Component\Console\Completion\CompletionInput; |
11 | 11 | use Symfony\Component\Console\Input\InputOption; |
| 12 | +use Symfony\Component\Console\Output\OutputInterface; |
12 | 13 | use Symfony\Component\Process\Exception\ExceptionInterface; |
13 | 14 | use Symfony\Component\Process\Exception\ProcessFailedException; |
14 | 15 | use Symfony\Component\Process\ExecutableFinder; |
|
22 | 23 | use function Castor\http_client; |
23 | 24 | use function Castor\io; |
24 | 25 | use function Castor\open; |
| 26 | +use function Castor\parallel; |
25 | 27 | use function Castor\run; |
26 | 28 | use function Castor\variable; |
27 | 29 | use function worktree\get_worktree_name; |
@@ -105,27 +107,90 @@ function build( |
105 | 107 |
|
106 | 108 | io()->title('Building infrastructure'); |
107 | 109 |
|
108 | | - $command = []; |
109 | | - |
110 | | - $command[] = '--profile'; |
111 | | - if ($profile) { |
112 | | - $command[] = $profile; |
113 | | - } else { |
114 | | - $command[] = '*'; |
115 | | - } |
116 | | - |
117 | 110 | $command = [ |
118 | | - ...$command, |
| 111 | + '--profile', $profile ?: '*', |
119 | 112 | 'build', |
120 | 113 | '--build-arg', 'PHP_VERSION=' . variable('php_version'), |
121 | 114 | '--build-arg', 'PROJECT_NAME=' . variable('project_name'), |
122 | 115 | ]; |
123 | 116 |
|
124 | 117 | if ($service) { |
125 | | - $command[] = $service; |
| 118 | + docker_compose([...$command, $service]); |
| 119 | + |
| 120 | + return; |
126 | 121 | } |
127 | 122 |
|
128 | | - docker_compose($command); |
| 123 | + // One build per service, in parallel, rather than all of them at once: in a single |
| 124 | + // BuildKit session, services sharing a stage (e.g. php-base) lose the cache of all |
| 125 | + // but one of them, see https://github.com/moby/buildkit/issues/6418 |
| 126 | + $services = array_filter(get_services($profile), static fn (array $config) => isset($config['build'])); |
| 127 | + |
| 128 | + // The services with a cache first, the others once they are built: a service |
| 129 | + // without cache sharing their stages (e.g. a worker) would build these stages from |
| 130 | + // scratch meanwhile, and these local layers prevent the others from finding the |
| 131 | + // next steps in their imported cache |
| 132 | + $waves = [ |
| 133 | + array_keys(array_filter($services, static fn (array $config) => isset($config['build']['cache_from']))), |
| 134 | + array_keys(array_filter($services, static fn (array $config) => !isset($config['build']['cache_from']))), |
| 135 | + ]; |
| 136 | + |
| 137 | + $c = context()->withPty(false)->withTty(false); |
| 138 | + |
| 139 | + foreach ($waves as $wave) { |
| 140 | + run_builds_in_parallel(array_combine($wave, array_map( |
| 141 | + static fn (string $service) => static fn (callable $callback) => docker_compose([...$command, $service], $c, callback: $callback), |
| 142 | + $wave, |
| 143 | + ))); |
| 144 | + } |
| 145 | +} |
| 146 | + |
| 147 | +/** |
| 148 | + * Runs the builds in parallel, prefixing each line of their output with the name of |
| 149 | + * their service: their outputs are mixed otherwise. |
| 150 | + * |
| 151 | + * @param array<string, \Closure(callable(string, string, Process): void): Process> $builds |
| 152 | + */ |
| 153 | +function run_builds_in_parallel(array $builds): void |
| 154 | +{ |
| 155 | + if (!$builds) { |
| 156 | + return; |
| 157 | + } |
| 158 | + |
| 159 | + $width = max(array_map(strlen(...), array_keys($builds))); |
| 160 | + |
| 161 | + parallel(...array_map( |
| 162 | + static fn (string $service, \Closure $build) => static function () use ($service, $build, $width): void { |
| 163 | + $prefix = str_pad("[{$service}]", $width + 2) . ' '; |
| 164 | + $write = static function (string $lines) use ($prefix): void { |
| 165 | + foreach (explode("\n", $lines) as $line) { |
| 166 | + // BuildKit separates its steps with empty lines, useless once prefixed |
| 167 | + if ('' !== trim($line)) { |
| 168 | + io()->write($prefix . $line . "\n", false, OutputInterface::OUTPUT_RAW); |
| 169 | + } |
| 170 | + } |
| 171 | + }; |
| 172 | + |
| 173 | + // Only complete lines are written, the end of each stream is kept until its next chunk |
| 174 | + $buffers = [Process::OUT => '', Process::ERR => '']; |
| 175 | + |
| 176 | + try { |
| 177 | + $build(static function (string $type, string $bytes) use (&$buffers, $write): void { |
| 178 | + $buffers[$type] .= str_replace("\r\n", "\n", $bytes); |
| 179 | + |
| 180 | + if (false !== $position = strrpos($buffers[$type], "\n")) { |
| 181 | + $write(substr($buffers[$type], 0, $position)); |
| 182 | + $buffers[$type] = substr($buffers[$type], $position + 1); |
| 183 | + } |
| 184 | + }); |
| 185 | + } finally { |
| 186 | + foreach (array_filter($buffers) as $buffer) { |
| 187 | + $write($buffer); |
| 188 | + } |
| 189 | + } |
| 190 | + }, |
| 191 | + array_keys($builds), |
| 192 | + $builds, |
| 193 | + )); |
129 | 194 | } |
130 | 195 |
|
131 | 196 | /** |
@@ -408,10 +473,11 @@ function get_compose_environment(Context $c): array |
408 | 473 | } |
409 | 474 |
|
410 | 475 | /** |
411 | | - * @param list<string> $subCommand |
412 | | - * @param list<string> $profiles |
| 476 | + * @param list<string> $subCommand |
| 477 | + * @param list<string> $profiles |
| 478 | + * @param (callable(string, string, Process): void)|null $callback Receives the output instead of the console |
413 | 479 | */ |
414 | | -function docker_compose(array $subCommand, ?Context $c = null, array $profiles = []): Process |
| 480 | +function docker_compose(array $subCommand, ?Context $c = null, array $profiles = [], ?callable $callback = null): Process |
415 | 481 | { |
416 | 482 | $c ??= context(); |
417 | 483 | $profiles = $profiles ?: ['default']; |
@@ -451,7 +517,7 @@ function docker_compose(array $subCommand, ?Context $c = null, array $profiles = |
451 | 517 |
|
452 | 518 | $command = array_merge($command, $subCommand); |
453 | 519 |
|
454 | | - return run($command, context: $c); |
| 520 | + return run($command, context: $c, callback: $callback); |
455 | 521 | } |
456 | 522 |
|
457 | 523 | /** |
@@ -614,35 +680,49 @@ function push( |
614 | 680 | $command[] = '--set'; |
615 | 681 | $command[] = '*.args.PHP_VERSION=' . $c['php_version']; |
616 | 682 |
|
| 683 | + if ($dryRun) { |
| 684 | + $command[] = '--print'; |
| 685 | + } |
| 686 | + |
| 687 | + // One bake per service, see build() |
| 688 | + $commands = []; |
617 | 689 | foreach ($cacheFroms as $service => $cacheFrom) { |
618 | | - $command[] = '--set'; |
619 | | - $command[] = "{$service}.cache-to={$cacheFrom},mode=max"; |
| 690 | + $serviceCommand = [...$command, '--set', "{$service}.cache-to={$cacheFrom},mode=max"]; |
620 | 691 |
|
621 | | - if (!$tag) { |
622 | | - continue; |
623 | | - } |
| 692 | + if ($tag) { |
| 693 | + // Image name without its tag, e.g. "ghcr.io/jolicode/docker-starter/php" |
| 694 | + $image = isset($services[$service]['image']) ? preg_replace('{:[^/]+$}', '', $services[$service]['image']) : "{$registry}/{$service}"; |
624 | 695 |
|
625 | | - // Image name without its tag, e.g. "ghcr.io/jolicode/docker-starter/php" |
626 | | - $image = isset($services[$service]['image']) ? preg_replace('{:[^/]+$}', '', $services[$service]['image']) : "{$registry}/{$service}"; |
| 696 | + foreach ($tag as $t) { |
| 697 | + $serviceCommand[] = '--set'; |
| 698 | + $serviceCommand[] = "{$service}.tags={$image}:{$t}"; |
| 699 | + } |
627 | 700 |
|
628 | | - foreach ($tag as $t) { |
629 | | - $command[] = '--set'; |
630 | | - $command[] = "{$service}.tags={$image}:{$t}"; |
| 701 | + $serviceCommand[] = '--set'; |
| 702 | + $serviceCommand[] = "{$service}.output=type=registry"; |
631 | 703 | } |
632 | 704 |
|
633 | | - $command[] = '--set'; |
634 | | - $command[] = "{$service}.output=type=registry"; |
| 705 | + $commands[$service] = [...$serviceCommand, $service]; |
635 | 706 | } |
636 | 707 |
|
637 | 708 | if ($dryRun) { |
638 | | - $command[] = '--print'; |
| 709 | + foreach ($commands as $serviceCommand) { |
| 710 | + run($serviceCommand, context: $c); |
| 711 | + } |
| 712 | + |
| 713 | + return; |
639 | 714 | } |
640 | 715 |
|
641 | | - run([...$command, ...array_keys($cacheFroms)], context: $c); |
| 716 | + $c = $c->withPty(false)->withTty(false); |
| 717 | + |
| 718 | + run_builds_in_parallel(array_map( |
| 719 | + static fn (array $serviceCommand) => static fn (callable $callback) => run($serviceCommand, context: $c, callback: $callback), |
| 720 | + $commands, |
| 721 | + )); |
642 | 722 | } |
643 | 723 |
|
644 | 724 | /** |
645 | | - * @return array<string, array{image?: string, profiles?: list<string>, build: array{context: string, dockerfile?: string, cache_from?: list<string>, target?: string, additional_contexts?: array<string, string>}}> |
| 725 | + * @return array<string, array{image?: string, profiles?: list<string>, build?: array{context: string, dockerfile?: string, cache_from?: list<string>, target?: string, additional_contexts?: array<string, string>}}> |
646 | 726 | */ |
647 | 727 | function get_services(?string $profile = null): array |
648 | 728 | { |
|
0 commit comments