55namespace CodedMonkey \Dirigent \Package ;
66
77use CodedMonkey \Dirigent \Composer \ComposerClient ;
8+ use CodedMonkey \Dirigent \Composer \ConfigFactory ;
89use CodedMonkey \Dirigent \Doctrine \Entity \Distribution ;
910use CodedMonkey \Dirigent \Doctrine \Entity \Metadata ;
1011use CodedMonkey \Dirigent \Doctrine \Repository \DistributionRepository ;
12+ use CodedMonkey \Dirigent \Entity \PackageFetchStrategy ;
1113use 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 ;
1220use Symfony \Component \DependencyInjection \Attribute \Autowire ;
1321use Symfony \Component \Filesystem \Filesystem ;
1422use 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}
0 commit comments