55top entry in conandata.yml), filtered by the recipe's `platforms` list. The
66first failure stops the run and prints a summary. Uploading is opt-in and only
77runs once every recipe has built successfully.
8+
9+ With --export-only the script just runs 'conan export' for every version of
10+ every recipe, so a subsequent 'conan install --build=missing' resolves recipes
11+ from the local cache and builds whatever the remote has no binaries for. CI
12+ uses this to validate recipe changes together with the engine build.
813"""
914
1015from __future__ import annotations
@@ -45,7 +50,8 @@ def __init__(self, path: Path):
4550
4651 data = yaml .safe_load (self .conandata .read_text (encoding = "utf-8" )) or {}
4752 self .name = self ._parse_name () or self .dir_name
48- self .version = latest_version (data )
53+ self .versions = list_versions (data )
54+ self .version = self .versions [0 ] if self .versions else None
4955 self .platforms = data .get ("platforms" ) or []
5056 self .requires = data .get ("requires" ) or []
5157
@@ -62,14 +68,14 @@ def reference(self) -> str:
6268 return f"{ self .name } /{ self .version } "
6369
6470
65- def latest_version (data : dict ) -> str | None :
71+ def list_versions (data : dict ) -> list [ str ] :
6672 sources = data .get ("sources" )
6773 if isinstance (sources , dict ) and sources :
68- return next ( iter ( sources ))
74+ return [ str ( v ) for v in sources ]
6975 versions = data .get ("versions" )
7076 if isinstance (versions , list ) and versions :
71- return versions [ 0 ]
72- return None
77+ return [ str ( v ) for v in versions ]
78+ return []
7379
7480
7581def discover_recipes (root : Path ) -> list [Recipe ]:
@@ -104,8 +110,9 @@ def visit(recipe: Recipe):
104110 return ordered
105111
106112
107- def run (cmd : list [str ], cwd : Path | None = None ) -> int :
108- print (f"\n $ { ' ' .join (cmd )} " , flush = True )
113+ def run (cmd : list [str ], cwd : Path | None = None , redact : set [str ] | None = None ) -> int :
114+ shown = " " .join ("***" if redact and arg in redact else arg for arg in cmd )
115+ print (f"\n $ { shown } " , flush = True )
109116 return subprocess .run (cmd , cwd = str (cwd ) if cwd else None ).returncode
110117
111118
@@ -140,6 +147,11 @@ def parse_args() -> argparse.Namespace:
140147 parser .add_argument (
141148 "--skip" , action = "append" , default = [], help = "skip these recipe names (repeatable)"
142149 )
150+ parser .add_argument (
151+ "--export-only" ,
152+ action = "store_true" ,
153+ help = "only 'conan export' every version of every recipe; no build, no platform filter" ,
154+ )
143155
144156 upload = parser .add_argument_group ("upload" )
145157 upload .add_argument (
@@ -157,9 +169,24 @@ def parse_args() -> argparse.Namespace:
157169 args = parser .parse_args ()
158170 if args .upload and not args .remote :
159171 parser .error ("--upload requires --remote" )
172+ if args .export_only and args .upload :
173+ parser .error ("--export-only cannot be combined with --upload" )
160174 return args
161175
162176
177+ def export_all (args : argparse .Namespace , recipes : list [Recipe ]):
178+ count = 0
179+ for recipe in recipes :
180+ if not recipe .versions :
181+ sys .exit (f"error: could not determine versions from { recipe .conandata } " )
182+ for version in recipe .versions :
183+ cmd = [args .conan , "export" , f"{ recipe .dir_name } /conanfile.py" , "--version" , version ]
184+ if run (cmd , cwd = args .recipes_root ) != 0 :
185+ sys .exit (f"error: failed to export { recipe .name } /{ version } " )
186+ count += 1
187+ print (f"\n Exported { count } recipe version(s)." , flush = True )
188+
189+
163190def build_all (args : argparse .Namespace , recipes : list [Recipe ], host : str ):
164191 built : list [Recipe ] = []
165192 skipped : list [tuple [Recipe , str ]] = []
@@ -215,9 +242,11 @@ def upload_all(args: argparse.Namespace, built: list[Recipe]):
215242
216243 if args .remote_user is not None :
217244 login = [args .conan , "remote" , "login" , args .remote , args .remote_user ]
245+ redact : set [str ] = set ()
218246 if args .remote_password is not None :
219247 login += ["-p" , args .remote_password ]
220- if run (login ) != 0 :
248+ redact .add (args .remote_password )
249+ if run (login , redact = redact ) != 0 :
221250 sys .exit ("error: failed to log in to remote" )
222251
223252 for recipe in built :
@@ -250,9 +279,6 @@ def main():
250279 if not root .is_dir ():
251280 sys .exit (f"error: recipes root not found: { root } " )
252281
253- host = current_platform ()
254- print (f"Host platform: { host } " , flush = True )
255-
256282 recipes = discover_recipes (root )
257283 if args .only :
258284 recipes = [r for r in recipes if r .name in args .only or r .dir_name in args .only ]
@@ -261,6 +287,13 @@ def main():
261287 if not recipes :
262288 sys .exit ("error: no recipes to build" )
263289
290+ if args .export_only :
291+ export_all (args , recipes )
292+ return
293+
294+ host = current_platform ()
295+ print (f"Host platform: { host } " , flush = True )
296+
264297 recipes = order_by_dependencies (recipes )
265298 print ("Build order: " + ", " .join (r .name for r in recipes ), flush = True )
266299
0 commit comments