Skip to content

Commit a88b136

Browse files
committed
Add distribution builder
1 parent 927ad0c commit a88b136

2 files changed

Lines changed: 119 additions & 21 deletions

File tree

src/Package/PackageDistributionResolver.php

Lines changed: 114 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,18 @@
55
namespace CodedMonkey\Dirigent\Package;
66

77
use CodedMonkey\Dirigent\Composer\ComposerClient;
8+
use CodedMonkey\Dirigent\Composer\ConfigFactory;
89
use CodedMonkey\Dirigent\Doctrine\Entity\Distribution;
910
use CodedMonkey\Dirigent\Doctrine\Entity\Metadata;
1011
use CodedMonkey\Dirigent\Doctrine\Repository\DistributionRepository;
12+
use CodedMonkey\Dirigent\Entity\PackageFetchStrategy;
1113
use CodedMonkey\Dirigent\Message\ResolveDistribution;
14+
use Composer\IO\NullIO;
15+
use Composer\Pcre\Preg;
16+
use Composer\Util\Filesystem as ComposerFilesystem;
17+
use Composer\Util\Git as GitUtility;
18+
use Composer\Util\ProcessExecutor;
19+
use Composer\Util\Url;
1220
use Symfony\Component\DependencyInjection\Attribute\Autowire;
1321
use Symfony\Component\Filesystem\Filesystem;
1422
use Symfony\Component\Filesystem\Path;
@@ -27,6 +35,8 @@ public function __construct(
2735
private ComposerClient $composer,
2836
private DistributionRepository $distributionRepository,
2937
private LockFactory $lockFactory,
38+
#[Autowire(param: 'dirigent.distributions.build')]
39+
private bool $buildDistributions,
3040
#[Autowire(param: 'dirigent.distributions.mirror')]
3141
private bool $mirrorDistributions,
3242
#[Autowire(param: 'dirigent.distributions.dev_versions')]
@@ -87,21 +97,19 @@ public function removeFile(string $relativePath): void
8797

8898
public function resolve(Metadata $metadata, string $type, bool $async): bool
8999
{
90-
if (!$this->mirrorDistributions) {
91-
return false;
92-
}
93-
94100
$path = $this->path($metadata, $type);
95101

96102
if ($this->fileExists($path)) {
97103
return true;
98104
}
99105

100-
if ($type !== $metadata->getDistributionType()) {
106+
if (null === $strategy = $this->getFetchStrategy($metadata)) {
101107
return false;
102108
}
103109

104-
if ($metadata->getVersion()->isDevelopment() && !$this->includeDevVersions) {
110+
$currentType = $strategy->isMirror() ? $metadata->getDistributionType() : 'zip';
111+
112+
if ($type !== $currentType) {
105113
return false;
106114
}
107115

@@ -115,9 +123,6 @@ public function resolve(Metadata $metadata, string $type, bool $async): bool
115123
return false;
116124
}
117125

118-
$distributionUrl = $metadata->getDistributionUrl();
119-
$path = $this->path($metadata, $type);
120-
121126
$lock = $this->createDistributionLock($path);
122127

123128
try {
@@ -129,29 +134,93 @@ public function resolve(Metadata $metadata, string $type, bool $async): bool
129134
$distribution = new Distribution($metadata, $type);
130135
}
131136

132-
$this->filesystem->mkdir(dirname($path));
137+
$result = false;
138+
139+
// Build the distribution from VCS source
140+
if ($strategy->isVcs()) {
141+
$result = $this->build($distribution);
142+
143+
if (
144+
!$result
145+
&& $this->mirrorDistributions
146+
&& $metadata->hasDistribution()
147+
&& $type === $metadata->getDistributionType()
148+
) {
149+
// Mirror the distribution if it failed to build from source
150+
// todo log fallback
151+
$strategy = PackageFetchStrategy::Mirror;
152+
}
153+
}
133154

134-
$httpDownloader = $this->composer->createHttpDownloader();
135-
$httpDownloader->copy($distributionUrl, $path);
155+
if ($strategy->isMirror()) {
156+
$result = $this->mirror($distribution, $path);
157+
}
136158

137-
$distribution->setSource($distributionUrl);
138-
$distribution->setResolvedAt();
159+
if ($result) {
160+
$distribution->setResolvedAt();
139161

140-
try {
141-
$this->distributionRepository->save($distribution, true);
142-
} catch (\Throwable $exception) {
143-
// Remove file immediately if saving the distribution to the database failed
144-
$this->removePath($path);
162+
try {
163+
$this->distributionRepository->save($distribution, true);
164+
} catch (\Throwable $exception) {
165+
// Remove file immediately if saving the distribution to the database failed
166+
$this->removePath($path);
145167

146-
throw $exception;
168+
throw $exception;
169+
}
147170
}
148171

149-
return true;
172+
return $result;
150173
} finally {
151174
$lock->release();
152175
}
153176
}
154177

178+
private function build(Distribution $distribution): bool
179+
{
180+
$metadata = $distribution->getMetadata();
181+
$reference = $distribution->getReference();
182+
183+
$package = $metadata->getPackage();
184+
$repositoryUrl = $package->getRepositoryUrl();
185+
$distributionPath = $this->path($metadata, $reference);
186+
187+
$composerConfig = ConfigFactory::createForVcsRepository($repositoryUrl, $package->getRepositoryCredentials());
188+
189+
$gitUtility = new GitUtility(
190+
$io = new NullIO(),
191+
$composerConfig,
192+
$process = new ProcessExecutor($io),
193+
new ComposerFilesystem($process),
194+
);
195+
196+
$cacheRepositoryName = Preg::replace('{[^a-z0-9.]}i', '-', Url::sanitize($repositoryUrl));
197+
$cachePath = $composerConfig->get('cache-vcs-dir') . '/' . $cacheRepositoryName . '/';
198+
199+
$this->filesystem->mkdir(dirname($distributionPath));
200+
201+
$gitUtility->runCommands([
202+
['git', 'archive', '--format=zip', "--output=$distributionPath", $reference],
203+
], $repositoryUrl, $cachePath);
204+
205+
$distribution->setSource(null);
206+
207+
return true;
208+
}
209+
210+
private function mirror(Distribution $distribution, string $path): bool
211+
{
212+
$url = $distribution->getMetadata()->getDistributionUrl();
213+
214+
$this->filesystem->mkdir(dirname($path));
215+
216+
$httpDownloader = $this->composer->createHttpDownloader();
217+
$httpDownloader->copy($url, $path);
218+
219+
$distribution->setSource($url);
220+
221+
return true;
222+
}
223+
155224
private function createDistributionLock(string $path): SharedLockInterface
156225
{
157226
$lock = $this->lockFactory->createLock('distribution.' . hash('sha256', $path), ttl: null);
@@ -160,6 +229,9 @@ private function createDistributionLock(string $path): SharedLockInterface
160229
return $lock;
161230
}
162231

232+
/**
233+
* @phpstan-impure
234+
*/
163235
private function fileExists(string $path): bool
164236
{
165237
return $this->filesystem->exists($path);
@@ -188,4 +260,25 @@ private function encodePathComponent(string $component): string
188260
default => $encodedComponent,
189261
};
190262
}
263+
264+
private function getFetchStrategy(Metadata $metadata): ?PackageFetchStrategy
265+
{
266+
$fetchStrategy = $metadata->getPackage()->getFetchStrategy();
267+
268+
if (!$this->includeDevVersions && $metadata->getVersion()->isDevelopment()) {
269+
// Development versions are not supported by the configuration
270+
return null;
271+
} elseif ($this->buildDistributions && $fetchStrategy->isVcs()) {
272+
// Only build distributions if the fetch strategy is VCS (not source because it might not contain VCS data)
273+
return PackageFetchStrategy::Vcs;
274+
} elseif (
275+
$this->mirrorDistributions
276+
&& ($fetchStrategy->isMirror() || $metadata->hasDistribution())
277+
) {
278+
// Always mirror distributions if building from source is not supported and a distribution is available
279+
return PackageFetchStrategy::Mirror;
280+
}
281+
282+
return null;
283+
}
191284
}

tests/UnitTests/Package/PackageDistributionResolverTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public function testPathEncodesMetadataAndKeepsTraversalReferenceInsideStorage()
5151
$this->createStub(LockFactory::class),
5252
true,
5353
true,
54+
true,
5455
$this->storagePath,
5556
);
5657

@@ -75,6 +76,7 @@ public function testRemoveFileDeletesDistributionFile(): void
7576
$lockFactory = $this->createMock(LockFactory::class),
7677
true,
7778
true,
79+
true,
7880
$this->storagePath,
7981
);
8082

@@ -108,6 +110,7 @@ public function testRemoveFileKeepsNonEmptyPackageDirectory(): void
108110
$this->createStub(LockFactory::class),
109111
true,
110112
true,
113+
true,
111114
$this->storagePath,
112115
);
113116

@@ -138,6 +141,7 @@ public function testRemoveFileWithTraversalReferenceDoesNotDeleteOutsideDistribu
138141
$lockFactory,
139142
true,
140143
true,
144+
true,
141145
$this->storagePath,
142146
);
143147

@@ -193,6 +197,7 @@ public function testResolveDeletesDownloadedFileWhenPersistenceFails(): void
193197
$lockFactory,
194198
true,
195199
true,
200+
true,
196201
$this->storagePath,
197202
);
198203
$path = $resolver->path($metadata, 'zip');

0 commit comments

Comments
 (0)