Skip to content

Commit d4e19eb

Browse files
committed
Symlink packages after composer update
This is a fundamental rewrite of the Composer integration. Now, instead of adding the loaded paths to Composer's search path (by creating path repositories for them), we replace the packages downloaded by Composer that can be found in the loaded paths by symlinks to the local paths. Doing so requires us to hook into the autoload dumper, which now has to respect the rules in the local path, not those obtained from Packagist. All of this should hopefully fix several issues, most importantly: - Composer's lock file will be written before Studio does its magic, therefore not causing any conflicts with other developers' setups. - Different version constraints on symlinked packages won't cause problems anymore. Any required packages that are found in loaded paths will be loaded, no matter the branch or version they are on. Open questions: - How should packages be handled that have not yet been added to Packagist? (Proposed solution: Create path repositories for the loaded paths, but *append* them instead of *prepending*, so that they will only be used as fallback, if Packagist does not yield any results.) - Should we validate the constraints from composer.json before creating symlinks? With this setup, everything might be working locally, but not when downloading the package from Packagist (as another version may be downloaded instead). Refs #52, #58, #65, #72.
1 parent 4c8784d commit d4e19eb

File tree

1 file changed

+111
-12
lines changed

1 file changed

+111
-12
lines changed

src/Composer/StudioPlugin.php

Lines changed: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@
55
use Composer\Composer;
66
use Composer\EventDispatcher\EventSubscriberInterface;
77
use Composer\IO\IOInterface;
8+
use Composer\Json\JsonFile;
9+
use Composer\Package\PackageInterface;
810
use Composer\Plugin\PluginInterface;
11+
use Composer\Repository\CompositeRepository;
12+
use Composer\Repository\InstalledFilesystemRepository;
913
use Composer\Repository\PathRepository;
14+
use Composer\Repository\WritableRepositoryInterface;
1015
use Composer\Script\ScriptEvents;
16+
use Composer\Util\Filesystem;
1117
use Studio\Config\Config;
12-
use Studio\Config\FileStorage;
1318

1419
class StudioPlugin implements PluginInterface, EventSubscriberInterface
1520
{
@@ -31,32 +36,121 @@ public function activate(Composer $composer, IOInterface $io)
3136

3237
public static function getSubscribedEvents()
3338
{
39+
// TODO: Before update, append Studio path repositories
3440
return [
35-
ScriptEvents::PRE_INSTALL_CMD => 'registerStudioPackages',
36-
ScriptEvents::PRE_UPDATE_CMD => 'registerStudioPackages',
41+
ScriptEvents::POST_UPDATE_CMD => 'symlinkStudioPackages',
42+
ScriptEvents::PRE_AUTOLOAD_DUMP => 'loadStudioPackagesForDump',
3743
];
3844
}
3945

4046
/**
41-
* Register all managed paths with Composer.
47+
* Symlink all Studio-managed packages
4248
*
43-
* This function configures Composer to treat all Studio-managed paths as local path repositories, so that packages
44-
* therein will be symlinked directly.
49+
* After `composer update`, we replace all packages that can also be found
50+
* in paths managed by Studio with symlinks to those paths.
4551
*/
46-
public function registerStudioPackages()
52+
public function symlinkStudioPackages()
53+
{
54+
$intersection = $this->getManagedPackages();
55+
56+
// Create symlinks for all left-over packages in vendor/composer/studio
57+
$destination = $this->composer->getConfig()->get('vendor-dir') . '/composer/studio';
58+
(new Filesystem())->emptyDirectory($destination);
59+
$studioRepo = new InstalledFilesystemRepository(
60+
new JsonFile($destination . '/installed.json')
61+
);
62+
63+
$installationManager = $this->composer->getInstallationManager();
64+
65+
// Get local repository which contains all installed packages
66+
$installed = $this->composer->getRepositoryManager()->getLocalRepository();
67+
68+
foreach ($intersection as $package) {
69+
$original = $installed->findPackage($package->getName(), '*');
70+
71+
$installationManager->getInstaller($original->getType())
72+
->uninstall($installed, $original);
73+
74+
$installationManager->getInstaller($package->getType())
75+
->install($studioRepo, $package);
76+
}
77+
78+
$studioRepo->write();
79+
80+
// TODO: Run dump-autoload again
81+
}
82+
83+
public function loadStudioPackagesForDump()
84+
{
85+
$localRepo = $this->composer->getRepositoryManager()->getLocalRepository();
86+
$intersection = $this->getManagedPackages();
87+
88+
$packagesToReplace = [];
89+
foreach ($intersection as $package) {
90+
$packagesToReplace[] = $package->getName();
91+
}
92+
93+
// Remove all packages with same names as one of symlinked packages
94+
$packagesToRemove = [];
95+
foreach ($localRepo->getCanonicalPackages() as $package) {
96+
if (in_array($package->getName(), $packagesToReplace)) {
97+
$packagesToRemove[] = $package;
98+
}
99+
}
100+
foreach ($packagesToRemove as $package) {
101+
$localRepo->removePackage($package);
102+
}
103+
104+
// Add symlinked packages to local repository
105+
foreach ($intersection as $package) {
106+
$localRepo->addPackage(clone $package);
107+
}
108+
}
109+
110+
/**
111+
* @param WritableRepositoryInterface $installedRepo
112+
* @param PathRepository[] $managedRepos
113+
* @return PackageInterface[]
114+
*/
115+
private function getIntersection(WritableRepositoryInterface $installedRepo, $managedRepos)
116+
{
117+
$managedRepo = new CompositeRepository($managedRepos);
118+
119+
return array_filter(
120+
array_map(
121+
function (PackageInterface $package) use ($managedRepo) {
122+
return $managedRepo->findPackage($package->getName(), '*');
123+
},
124+
$installedRepo->getCanonicalPackages()
125+
)
126+
);
127+
}
128+
129+
private function getManagedPackages()
47130
{
48-
$repoManager = $this->composer->getRepositoryManager();
49131
$composerConfig = $this->composer->getConfig();
50132

133+
// Get array of PathRepository instances for Studio-managed paths
134+
$managed = [];
51135
foreach ($this->getManagedPaths() as $path) {
52-
$this->io->writeError("[Studio] Loading path $path");
53-
54-
$repoManager->prependRepository(new PathRepository(
136+
$managed[] = new PathRepository(
55137
['url' => $path],
56138
$this->io,
57139
$composerConfig
58-
));
140+
);
141+
}
142+
143+
// Intersect PathRepository packages with local repository
144+
$intersection = $this->getIntersection(
145+
$this->composer->getRepositoryManager()->getLocalRepository(),
146+
$managed
147+
);
148+
149+
foreach ($intersection as $package) {
150+
$this->write('Loading package ' . $package->getUniqueName());
59151
}
152+
153+
return $intersection;
60154
}
61155

62156
/**
@@ -71,4 +165,9 @@ private function getManagedPaths()
71165

72166
return $config->getPaths();
73167
}
168+
169+
private function write($msg)
170+
{
171+
$this->io->writeError("[Studio] $msg");
172+
}
74173
}

0 commit comments

Comments
 (0)