‪TYPO3CMS  ‪main
PackageArtifactBuilder.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Composer\IO\IOInterface;
21 use Composer\Package\PackageInterface;
22 use Composer\Repository\PlatformRepository;
23 use Composer\Script\Event;
24 use Composer\Util\Filesystem;
25 use Composer\Util\Platform;
26 use Symfony\Component\Filesystem\Exception\IOException;
27 use TYPO3\CMS\Composer\Plugin\Config;
28 use TYPO3\CMS\Composer\Plugin\Core\InstallerScript;
29 use TYPO3\CMS\Composer\Plugin\Util\ExtensionKeyResolver;
30 use TYPO3\CMS\Core\Package\Cache\ComposerPackageArtifact;
37 use TYPO3\CMS\Core\Package\PackageManager;
41 
54 class ‪PackageArtifactBuilder extends PackageManager implements InstallerScript
55 {
59  private ‪$event;
60 
64  private ‪$config;
65 
69  private ‪$fileSystem;
70 
76 
77  public function ‪__construct()
78  {
79  // Disable path determination with Environment class, which is not initialized here
80  parent::__construct(new ‪DependencyOrderingService(), '', '');
81  }
82 
83  protected function ‪isComposerDependency(string $packageName): bool
84  {
85  return PlatformRepository::isPlatformPackage($packageName) || ($this->availableComposerPackageKeys[$packageName] ?? false);
86  }
87 
96  public function ‪run(Event ‪$event): bool
97  {
98  $io = ‪$event->getIO();
99  $this->event = ‪$event;
100  $this->config = Config::load($this->event->getComposer(), $io);
101  $this->fileSystem = new Filesystem();
102  $composer = $this->event->getComposer();
103  $basePath = $this->config->get('base-dir');
104  $this->packagesBasePath = $basePath . '/';
105  $installedTypo3Packages = $this->‪extractPackageMapFromComposer();
106  $messages = $this->publishResources($installedTypo3Packages);
107  foreach ($messages as $message) {
108  $io->writeError(
109  $this->‪formatMessage($message),
110  true,
111  $message['verbosity'],
112  );
113  }
114  foreach ($installedTypo3Packages as [$composerPackage, $path, $extensionKey]) {
115  $packagePath = ‪PathUtility::sanitizeTrailingSeparator($path);
116  $package = new Package($this, $extensionKey, $packagePath, true);
117  $this->‪setTitleFromExtEmConf($package);
118  $package->makePathRelative($this->fileSystem, $basePath);
119  $package->getPackageMetaData()->setVersion($composerPackage->getPrettyVersion());
120  $this->registerPackage($package);
121  }
123  $cacheIdentifier = md5(serialize($composer->getLocker()->getLockData()) . $this->event->isDevMode());
124  $this->setPackageCache(new ComposerPackageArtifact($composer->getConfig()->get('vendor-dir') . '/typo3', $this->fileSystem, $cacheIdentifier));
125  $this->saveToPackageCache();
126 
127  return true;
128  }
129 
134  private function ‪setTitleFromExtEmConf(Package $package): void
135  {
136  $emConfPath = $package->getPackagePath() . '/ext_emconf.php';
137  if (file_exists($emConfPath)) {
138  $_EXTKEY = $package->getPackageKey();
139  ‪$EM_CONF = null;
140  include $emConfPath;
141  if (!empty(‪$EM_CONF[$_EXTKEY]['title'])) {
142  $package->getPackageMetaData()->setTitle(‪$EM_CONF[$_EXTKEY]['title']);
143  }
144  }
145  }
146 
150  private function ‪sortPackagesAndConfiguration(): void
151  {
152  $packagesWithDependencies = $this->resolvePackageDependencies($this->packages);
153  // Sort the packages by key at first, so we get a stable sorting of "equivalent" packages afterwards
154  ksort($packagesWithDependencies);
155  $sortedPackageKeys = $this->sortPackageStatesConfigurationByDependency($packagesWithDependencies);
156  $this->packageStatesConfiguration = [];
157  $sortedPackages = [];
158  foreach ($sortedPackageKeys as $packageKey) {
159  $sortedPackages[$packageKey] = $this->packages[$packageKey];
160  // The artifact does not need path information, so it is kept empty
161  // The keys must be present, though because the PackageManager implies than a
162  // package is active by this configuration array
163  $this->packageStatesConfiguration['packages'][$packageKey] = [];
164  }
165  $this->packages = $sortedPackages;
166  $this->packageStatesConfiguration['version'] = 5;
167  }
168 
175  private function ‪extractPackageMapFromComposer(): array
176  {
177  $composer = $this->event->getComposer();
178  $rootPackage = $composer->getPackage();
179  $autoLoadGenerator = $composer->getAutoloadGenerator();
180  $localRepo = $composer->getRepositoryManager()->getLocalRepository();
181  $usedExtensionKeys = [];
182 
183  return array_map(
184  function (array $packageAndPath) use ($rootPackage, &$usedExtensionKeys): array {
185  [$composerPackage, $packagePath] = $packageAndPath;
186  $packageName = $composerPackage->getName();
187  $packagePath = GeneralUtility::fixWindowsFilePath($packagePath);
188  try {
189  $extensionKey = ExtensionKeyResolver::resolve($composerPackage);
190  } catch (\Throwable $e) {
191  if (str_starts_with($composerPackage->getType(), 'typo3-cms-')) {
192  // This means we have a package of type extension, and it does not have the extension key set
193  // This only happens since version > 4.0 of the installer and must be propagated to become user facing
194  throw $e;
195  }
196  // In case we can not otherwise determine the extension key, we take the composer name
197  $extensionKey = $packageName;
198  }
199  if (isset($usedExtensionKeys[$extensionKey])) {
200  throw new \UnexpectedValueException(
201  sprintf(
202  'Package with the name "%s" registered extension key "%s", but this key was already set by package with the name "%s"',
203  $packageName,
204  $extensionKey,
205  $usedExtensionKeys[$extensionKey]
206  ),
207  1638880941
208  );
209  }
210  $usedExtensionKeys[$extensionKey] = $packageName;
211  unset($this->availableComposerPackageKeys[$packageName]);
212  $this->composerNameToPackageKeyMap[$packageName] = $extensionKey;
213  if ($composerPackage === $rootPackage) {
214  // The root package's path is the Composer base dir
215  $packagePath = $this->config->get('base-dir');
216  }
217  // Add extension key to the package map for later reference
218  return [$composerPackage, $packagePath, $extensionKey];
219  },
220  array_filter(
221  $autoLoadGenerator->buildPackageMap($composer->getInstallationManager(), $rootPackage, $localRepo->getCanonicalPackages()),
222  ‪function (array $packageAndPath): bool {
224  [$composerPackage] = $packageAndPath;
225  // Filter all Composer packages without typo3/cms definition, but keep all
226  // package names, to be able to ignore Composer only dependencies when ordering the packages
227  $this->availableComposerPackageKeys[$composerPackage->getName()] = true;
228  foreach ($composerPackage->getReplaces() as $link) {
229  $this->availableComposerPackageKeys[$link->getTarget()] = true;
230  }
231  return isset($composerPackage->getExtra()['typo3/cms']);
232  }
233  )
234  );
235  }
236 
241  private function ‪formatMessage(array $message): string
242  {
243  if ($message['severity'] === 'title') {
244  return sprintf('<info>%s</info>', $message['message']);
245  }
246 
247  return sprintf(
248  ' * <%2$s>%s</%2$s>',
249  sprintf(str_replace(chr(10), '</%1$s>' . chr(10) . ' <%1$s>', $message['message']), $message['severity']),
250  $message['severity'],
251  );
252  }
253 
258  private function publishResources(array $installedTypo3Packages): array
259  {
261  [
262  'severity' => 'title',
263  'verbosity' => IOInterface::NORMAL,
264  'message' => 'Publishing public assets of TYPO3 extensions',
265  ],
266  ];
267  ‪$baseDir = $this->config->get('base-dir');
268  foreach ($installedTypo3Packages as [$composerPackage, $path, $extensionKey]) {
269  $fileSystemResourcesPath = ($path === '' ? ‪$baseDir : $path) . '/Resources/Public';
270  $relativePath = substr($fileSystemResourcesPath, strlen(‪$baseDir));
271  if (!file_exists($fileSystemResourcesPath)) {
273  'severity' => 'info',
274  'verbosity' => IOInterface::VERBOSE,
275  'message' => sprintf(
276  'Skipping assets publishing for extension "%s",'
277  . chr(10) . 'because its public resources directory "%s" does not exist.',
278  $composerPackage->getName(),
279  '.' . $relativePath,
280  ),
281  ];
282  continue;
283  }
284  [$relativePrefix] = explode('Resources/Public', $relativePath);
285  $publicResourcesPath = $this->fileSystem->normalizePath($this->config->get('web-dir') . '/_assets/' . md5($relativePrefix));
286  $this->fileSystem->ensureDirectoryExists(dirname($publicResourcesPath));
287  try {
288  if (Platform::isWindows()) {
289  $this->‪ensureJunctionExists($fileSystemResourcesPath, $publicResourcesPath, $composerPackage);
290  } else {
291  $this->‪ensureSymlinkExists($fileSystemResourcesPath, $publicResourcesPath, $composerPackage);
292  }
295  'severity' => 'warning',
296  'verbosity' => IOInterface::NORMAL,
297  'message' => sprintf(
298  'Could not publish public resources for extension "%s" by using the "%s" strategy.'
299  . chr(10) . 'Check whether the target directory "%s" already exists'
300  . chr(10) . 'and Composer has permissions to write inside the "_assets" directory.',
301  $e->packageName,
302  $e->publishingStrategy,
303  '.' . substr($publicResourcesPath, strlen(‪$baseDir)),
304  ),
305  ];
306  }
307  }
309  'severity' => 'title',
310  'verbosity' => IOInterface::NORMAL,
311  'message' => 'Published public assets',
312  ];
313 
314  return ‪$publishingMessages;
315  }
316 
320  private function ‪ensureJunctionExists(string $target, string $junction, PackageInterface $package): void
321  {
322  $e = null;
323  if (!$this->fileSystem->isJunction($junction)) {
324  try {
325  $this->fileSystem->junction($target, $junction);
326  } catch (IOException $e) {
327  }
328  }
329 
330  if ($e !== null || realpath($target) !== realpath($junction)) {
332  'junction',
333  $package->getName(),
334  1717488535,
335  $e,
336  );
337  }
338  }
339 
343  private function ‪ensureSymlinkExists(string $target, string $link, PackageInterface $package): void
344  {
345  $success = true;
346  if (!$this->fileSystem->isSymlinkedDirectory($link)) {
347  $success = $this->fileSystem->relativeSymlink($target, $link);
348  }
349 
350  if (!$success || realpath($target) !== realpath($link)) {
352  'symlink',
353  $package->getName(),
354  1717488536,
355  );
356  }
357  }
358 }
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$publishingMessages
‪foreach($installedTypo3Packages as[$composerPackage, $path, $extensionKey]) $publishingMessages[]
Definition: PackageArtifactBuilder.php:304
‪TYPO3\CMS\Core\Utility\PathUtility
Definition: PathUtility.php:27
‪TYPO3\CMS\Core\Package\Package\getPackageMetaData
‪getPackageMetaData()
Definition: Package.php:230
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageManifestException
Definition: InvalidPackageManifestException.php:23
‪TYPO3\CMS\Core\Package\Package\getPackagePath
‪string getPackagePath()
Definition: Package.php:204
‪$EM_CONF
‪$EM_CONF[$_EXTKEY]
Definition: ext_emconf.php:3
‪TYPO3\CMS\Core\Utility\PathUtility\sanitizeTrailingSeparator
‪static sanitizeTrailingSeparator(string $path, string $separator='/')
Definition: PathUtility.php:204
‪TYPO3\CMS\Core\Package\Exception\InvalidPackagePathException
Definition: InvalidPackagePathException.php:23
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\sortPackagesAndConfiguration
‪sortPackagesAndConfiguration()
Definition: PackageArtifactBuilder.php:146
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$config
‪Config $config
Definition: PackageArtifactBuilder.php:62
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\ensureJunctionExists
‪ensureJunctionExists(string $target, string $junction, PackageInterface $package)
Definition: PackageArtifactBuilder.php:316
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$availableComposerPackageKeys
‪array $availableComposerPackageKeys
Definition: PackageArtifactBuilder.php:71
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$baseDir
‪$baseDir
Definition: PackageArtifactBuilder.php:263
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageKeyException
Definition: InvalidPackageKeyException.php:23
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\__construct
‪__construct()
Definition: PackageArtifactBuilder.php:73
‪TYPO3\CMS\Core\Package\Package
Definition: Package.php:30
‪TYPO3\CMS\Core\Service\DependencyOrderingService
Definition: DependencyOrderingService.php:32
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$event
‪Event $event
Definition: PackageArtifactBuilder.php:58
‪TYPO3\CMS\Core\function
‪static return function(ContainerConfigurator $container, ContainerBuilder $containerBuilder)
Definition: Services.php:17
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$fileSystem
‪Filesystem $fileSystem
Definition: PackageArtifactBuilder.php:66
‪TYPO3\CMS\Core\Package\Exception\PackageAssetsPublishingFailedException
Definition: PackageAssetsPublishingFailedException.php:21
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\formatMessage
‪string formatMessage(array $message)
Definition: PackageArtifactBuilder.php:237
‪TYPO3\CMS\Core\Package\Exception\InvalidPackageStateException
Definition: InvalidPackageStateException.php:23
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\setTitleFromExtEmConf
‪setTitleFromExtEmConf(Package $package)
Definition: PackageArtifactBuilder.php:130
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\isComposerDependency
‪isComposerDependency(string $packageName)
Definition: PackageArtifactBuilder.php:79
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\run
‪run(Event $event)
Definition: PackageArtifactBuilder.php:92
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\ensureSymlinkExists
‪ensureSymlinkExists(string $target, string $link, PackageInterface $package)
Definition: PackageArtifactBuilder.php:339
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder
Definition: PackageArtifactBuilder.php:55
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\$publishingMessages
‪return $publishingMessages
Definition: PackageArtifactBuilder.php:310
‪TYPO3\CMS\Core\Package\Package\getPackageKey
‪getPackageKey()
Definition: Package.php:176
‪TYPO3\CMS\Core\Composer\PackageArtifactBuilder\extractPackageMapFromComposer
‪packageMap extractPackageMapFromComposer()
Definition: PackageArtifactBuilder.php:171
‪TYPO3\CMS\Core\Composer
Definition: CliEntryPoint.php:18