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 \Entity \Package ;
1112use CodedMonkey \Dirigent \Doctrine \Entity \Version ;
1213use CodedMonkey \Dirigent \Doctrine \Repository \DistributionRepository ;
14+ use CodedMonkey \Dirigent \Entity \PackageFetchStrategy ;
1315use CodedMonkey \Dirigent \Message \ResolveDistribution ;
16+ use Composer \IO \NullIO ;
17+ use Composer \Pcre \Preg ;
18+ use Composer \Util \Filesystem as ComposerFilesystem ;
19+ use Composer \Util \Git as GitUtility ;
20+ use Composer \Util \ProcessExecutor ;
21+ use Composer \Util \Url ;
1422use Symfony \Component \DependencyInjection \Attribute \Autowire ;
1523use Symfony \Component \Filesystem \Filesystem ;
1624use Symfony \Component \Lock \LockFactory ;
@@ -28,6 +36,10 @@ public function __construct(
2836 private ComposerClient $ composer ,
2937 private DistributionRepository $ distributionRepository ,
3038 private LockFactory $ lockFactory ,
39+ #[Autowire(param: 'dirigent.distributions.build ' )]
40+ private bool $ buildDistributions ,
41+ #[Autowire(param: 'dirigent.distributions.mirror ' )]
42+ private bool $ mirrorDistributions ,
3143 #[Autowire(param: 'dirigent.distributions.dev_versions ' )]
3244 private bool $ includeDevVersions ,
3345 #[Autowire(param: 'dirigent.storage.path ' )]
@@ -37,7 +49,7 @@ public function __construct(
3749 $ this ->storagePath = "$ storagePath/distribution " ;
3850 }
3951
40- public function exists (Metadata |string $ metadataOrPath , string $ reference, string $ type ): bool
52+ public function exists (Metadata |string $ metadataOrPath , ? string $ reference = null , ? string $ type = null ): bool
4153 {
4254 if ($ metadataOrPath instanceof Metadata) {
4355 return $ this ->filesystem ->exists ($ this ->path ($ metadataOrPath , $ reference , $ type ));
@@ -102,15 +114,20 @@ public function removeVersion(Version $version): void
102114
103115 public function resolve (Metadata $ metadata , string $ reference , string $ type , bool $ async ): bool
104116 {
105- if ($ this ->exists ($ metadata , $ reference , $ type )) {
117+ $ path = $ this ->path ($ metadata , $ reference , $ type );
118+
119+ if ($ this ->exists ($ path )) {
106120 return true ;
107121 }
108122
109- if ($ reference !== $ metadata -> getDistributionReference () || $ type !== $ metadata -> getDistributionType ( )) {
123+ if (null === $ strategy = $ this -> getFetchStrategy ( $ metadata )) {
110124 return false ;
111125 }
112126
113- if ($ metadata ->getVersion ()->isDevelopment () && !$ this ->includeDevVersions ) {
127+ $ currentReference = $ strategy ->isMirror () ? $ metadata ->getDistributionReference () : $ metadata ->getSourceReference ();
128+ $ currentType = $ strategy ->isMirror () ? $ metadata ->getDistributionType () : 'zip ' ;
129+
130+ if ($ reference !== $ currentReference || $ type !== $ currentType ) {
114131 return false ;
115132 }
116133
@@ -124,41 +141,119 @@ public function resolve(Metadata $metadata, string $reference, string $type, boo
124141 return false ;
125142 }
126143
127- $ distributionUrl = $ metadata ->getDistributionUrl ();
128- $ path = $ this ->path ($ metadata , $ reference , $ type );
129-
130144 $ lock = $ this ->createDistributionLock ($ path );
131145
132146 try {
133- if ($ this ->filesystem ->exists ($ path )) {
134- return true ;
135- }
136-
137147 if (null === $ distribution = $ this ->distributionRepository ->findOneByReferenceAndType ($ metadata , $ reference , $ type )) {
138148 $ distribution = new Distribution ($ metadata , $ reference , $ type );
139149 }
140150
141- $ this ->filesystem ->mkdir (dirname ($ path ));
142-
143- $ httpDownloader = $ this ->composer ->createHttpDownloader ();
144- $ httpDownloader ->copy ($ distributionUrl , $ path );
151+ $ result = false ;
152+
153+ // Build the distribution from VCS source
154+ if ($ strategy ->isVcs ()) {
155+ $ result = $ this ->build ($ distribution );
156+
157+ if (
158+ !$ result
159+ && $ this ->mirrorDistributions
160+ && $ metadata ->hasDistribution ()
161+ && $ reference === $ metadata ->getDistributionReference ()
162+ && $ type === $ metadata ->getDistributionType ()
163+ ) {
164+ // Mirror the distribution if it failed to build from source
165+ // todo log fallback
166+ $ strategy = PackageFetchStrategy::Mirror;
167+ }
168+ }
145169
146- $ distribution ->setSource ($ distributionUrl );
147- $ distribution ->setResolvedAt ();
170+ if ($ strategy ->isMirror ()) {
171+ $ result = $ this ->mirror ($ distribution , $ path );
172+ }
148173
149- $ this ->distributionRepository ->save ($ distribution , true );
174+ if ($ result ) {
175+ $ distribution ->setResolvedAt ();
176+ $ this ->distributionRepository ->save ($ distribution , true );
177+ }
150178
151- return true ;
179+ return $ result ;
152180 } finally {
153181 $ lock ->release ();
154182 }
155183 }
156184
185+ private function build (Distribution $ distribution ): bool
186+ {
187+ $ metadata = $ distribution ->getMetadata ();
188+ $ reference = $ distribution ->getReference ();
189+
190+ $ package = $ metadata ->getPackage ();
191+ $ repositoryUrl = $ package ->getRepositoryUrl ();
192+ $ distributionPath = $ this ->path ($ metadata , $ reference , 'zip ' );
193+
194+ $ composerConfig = ConfigFactory::createForVcsRepository ($ repositoryUrl , $ package ->getRepositoryCredentials ());
195+
196+ $ gitUtility = new GitUtility (
197+ $ io = new NullIO (),
198+ $ composerConfig ,
199+ $ process = new ProcessExecutor ($ io ),
200+ new ComposerFilesystem ($ process ),
201+ );
202+
203+ $ cacheRepositoryName = Preg::replace ('{[^a-z0-9.]}i ' , '- ' , Url::sanitize ($ repositoryUrl ));
204+ $ cachePath = $ composerConfig ->get ('cache-vcs-dir ' ) . '/ ' . $ cacheRepositoryName . '/ ' ;
205+
206+ $ this ->filesystem ->mkdir (dirname ($ distributionPath ));
207+
208+ $ gitUtility ->runCommands ([
209+ ['git ' , 'archive ' , '--format=zip ' , "--output= $ distributionPath " , $ reference ],
210+ ], $ repositoryUrl , $ cachePath );
211+
212+ $ distribution ->setSource (null );
213+
214+ return true ;
215+ }
216+
217+ private function mirror (Distribution $ distribution , string $ path ): bool
218+ {
219+ $ url = $ distribution ->getMetadata ()->getDistributionUrl ();
220+
221+ $ this ->filesystem ->mkdir (dirname ($ path ));
222+
223+ $ httpDownloader = $ this ->composer ->createHttpDownloader ();
224+ $ httpDownloader ->copy ($ url , $ path );
225+
226+ $ distribution ->setSource ($ url );
227+
228+ return true ;
229+ }
230+
157231 private function createDistributionLock (string $ path ): SharedLockInterface
158232 {
159233 $ lock = $ this ->lockFactory ->createLock ('distribution. ' . hash ('sha256 ' , $ path ), ttl: null );
160234 $ lock ->acquire (blocking: true );
161235
162236 return $ lock ;
163237 }
238+
239+ private function getFetchStrategy (Metadata $ metadata ): ?PackageFetchStrategy
240+ {
241+ $ fetchStrategy = $ metadata ->getPackage ()->getFetchStrategy ();
242+
243+ if (!$ this ->includeDevVersions && $ metadata ->getVersion ()->isDevelopment ()) {
244+ // Development versions are not supported by the configuration
245+ return null ;
246+ } elseif ($ this ->buildDistributions && $ fetchStrategy ->isVcs ()) {
247+ // Only build distributions if the fetch strategy is VCS (not source because it might not contain VCS data)
248+ return PackageFetchStrategy::Vcs;
249+ } elseif (
250+ $ this ->mirrorDistributions
251+ && ($ fetchStrategy ->isMirror () || $ metadata ->hasDistribution ())
252+ ) {
253+ // Always mirror distributions if building from source is not supported and a distribution is available
254+ return PackageFetchStrategy::Mirror;
255+ }
256+
257+ return null ;
258+ }
164259}
0 commit comments