11#!/usr/bin/env python3
22"""Bypass compile and fetch binaries."""
33
4- import argparse
54import json
65import logging
76import os
8- import re
97import sys
108import tarfile
119from tempfile import TemporaryDirectory
1210import urllib .error
1311import urllib .parse
1412import urllib .request
1513
16- # pylint: disable=ungrouped-imports
17- try :
18- from urllib .parse import urlparse
19- except ImportError :
20- from urllib .parse import urlparse # type: ignore
21- # pylint: enable=ungrouped-imports
14+ import click
2215
16+ from evergreen .api import RetryingEvergreenApi
2317from git .repo import Repo
2418import requests
2519import structlog
2620from structlog .stdlib import LoggerFactory
2721import yaml
2822
23+ EVG_CONFIG_FILE = ".evergreen.yml"
24+
2925# Get relative imports to work when the package is not installed on the PYTHONPATH.
3026if __name__ == "__main__" and __package__ is None :
3127 sys .path .append (os .path .dirname (os .path .dirname (os .path .abspath (__file__ ))))
4036_IS_WINDOWS = (sys .platform == "win32" or sys .platform == "cygwin" )
4137
4238# If changes are only from files in the bypass_files list or the bypass_directories list, then
43- # bypass compile, unless they are also found in the requires_compile_directories lists. All
44- # other file changes lead to compile.
39+ # bypass compile, unless they are also found in the BYPASS_EXTRA_CHECKS_REQUIRED lists. All other
40+ # file changes lead to compile.
4541BYPASS_WHITELIST = {
4642 "files" : {
4743 "etc/evergreen.yml" ,
@@ -114,19 +110,6 @@ def requests_get_json(url):
114110 raise
115111
116112
117- def read_evg_config ():
118- """Attempt to parse the Evergreen configuration from its home location.
119-
120- Return None if the configuration file wasn't found.
121- """
122- evg_file = os .path .expanduser ("~/.evergreen.yml" )
123- if os .path .isfile (evg_file ):
124- with open (evg_file , "r" ) as fstream :
125- return yaml .safe_load (fstream )
126-
127- return None
128-
129-
130113def write_out_bypass_compile_expansions (patch_file , ** expansions ):
131114 """Write out the macro expansions to given file."""
132115 with open (patch_file , "w" ) as out_file :
@@ -251,16 +234,17 @@ def _file_in_group(filename, group):
251234 return False
252235
253236
254- def should_bypass_compile (args ):
237+ def should_bypass_compile (patch_file , build_variant ):
255238 """
256239 Determine whether the compile stage should be bypassed based on the modified patch files.
257240
258241 We use lists of files and directories to more precisely control which modified patch files will
259242 lead to compile bypass.
260- :param args: Command line arguments.
243+ :param patch_file: A list of all files modified in patch build.
244+ :param build_variant: Build variant where compile is running.
261245 :returns: True if compile should be bypassed.
262246 """
263- with open (args . patchFile , "r" ) as pch :
247+ with open (patch_file , "r" ) as pch :
264248 for filename in pch :
265249 filename = filename .rstrip ()
266250 # Skip directories that show up in 'git diff HEAD --name-only'.
@@ -277,116 +261,100 @@ def should_bypass_compile(args):
277261 return False
278262
279263 if filename in BYPASS_EXTRA_CHECKS_REQUIRED :
280- if not _check_file_for_bypass (filename , args . buildVariant ):
264+ if not _check_file_for_bypass (filename , build_variant ):
281265 log .warning ("Compile bypass disabled due to extra checks for file." )
282266 return False
283267
284268 return True
285269
286270
287- def parse_args ():
288- """Parse the program arguments."""
289- parser = argparse .ArgumentParser ()
290- parser .add_argument ("--project" , required = True ,
291- help = "The Evergreen project. e.g mongodb-mongo-master" )
292-
293- parser .add_argument ("--buildVariant" , required = True ,
294- help = "The build variant. e.g enterprise-rhel-62-64-bit" )
295-
296- parser .add_argument ("--revision" , required = True , help = "The base commit hash." )
297-
298- parser .add_argument ("--patchFile" , required = True ,
299- help = "A list of all files modified in patch build." )
300-
301- parser .add_argument ("--outFile" , required = True ,
302- help = "The YAML file to write out the macro expansions." )
303-
304- parser .add_argument ("--jsonArtifact" , required = True ,
305- help = "The JSON file to write out the metadata of files to attach to task." )
306-
307- return parser .parse_args ()
308-
309-
310- def find_suitable_build_id (builds , args ):
271+ def find_build_for_previous_compile_task (evergreen_api , revision , project , build_variant ):
311272 """
312- Find a build_id that fits the given parameters .
273+ Find build_id of the base revision .
313274
314- :param builds: List of builds.
315- :param args: The parameters a build must meet, including project, buildVariant, and revision.
316- :return: Build_id that matches the parameters.
275+ :param evergreen_api: Evergreen.py object.
276+ :param revision: The base revision being run against.
277+ :param project: The evergreen project.
278+ :param build_variant: The build variant whose artifacts we want to use.
279+ :return: build_id of the base revision.
317280 """
318- prefix = "{}_{}_{}_" .format (args .project , args .buildVariant , args .revision )
319- # The "project" and "buildVariant" passed in may contain "-", but the "builds" listed from
320- # Evergreen only contain "_". Replace the hyphens before searching for the build.
321- prefix = prefix .replace ("-" , "_" )
322- build_id_pattern = re .compile (prefix )
323- for build_id in builds :
324- if build_id_pattern .search (build_id ):
325- return build_id
326- return None
327-
281+ project_prefix = project .replace ("-" , "_" )
282+ version_of_base_revision = "{}_{}" .format (project_prefix , revision )
283+ version = evergreen_api .version_by_id (version_of_base_revision )
284+ build_id = version .build_by_variant (build_variant ).id
285+ return build_id
328286
329- def main (): # pylint: disable=too-many-locals,too-many-statements
330- """Execute Main entry.
331287
332- From the /rest/v1/projects/{project}/revisions/{revision} endpoint find an existing build id
333- to generate the compile task id to use for retrieving artifacts when bypassing compile.
334-
335- We retrieve the URLs to the artifacts from the task info endpoint at
336- /rest/v1/tasks/{build_id}. We only download the artifacts.tgz and extract certain files
337- in order to retain any modified patch files.
288+ def find_previous_compile_task (evergreen_api , build_id , revision ):
289+ """
290+ Find compile task that should be used for skip compile..
338291
339- If for any reason bypass compile is false, we do not write out the macro expansion. Only if we
340- determine to bypass compile do we write out the macro expansions.
292+ :param evergreen_api: Evergreen.py object.
293+ :param build_id: Build id of the desired compile task.
294+ :param revision: The base revision being run against.
295+ :return: Evergreen.py object containing data about the desired compile task.
296+ """
297+ index = build_id .find (revision )
298+ compile_task_id = "{}compile_{}" .format (build_id [:index ], build_id [index :])
299+ task = evergreen_api .task_by_id (compile_task_id )
300+ return task
301+
302+
303+ @click .command ()
304+ @click .option ("--project" , required = True , help = "The evergreen project." )
305+ @click .option ("--build-variant" , required = True ,
306+ help = "The build variant whose artifacts we want to use." )
307+ @click .option ("--revision" , required = True , help = "Base revision of the build." )
308+ @click .option ("--patch-file" , required = True , help = "A list of all files modified in patch build." )
309+ @click .option ("--out-file" , required = True , help = "File to write expansions to." )
310+ @click .option ("--json-artifact" , required = True ,
311+ help = "The JSON file to write out the metadata of files to attach to task." )
312+ def main ( # pylint: disable=too-many-arguments,too-many-locals,too-many-statements
313+ project , build_variant , revision , patch_file , out_file , json_artifact ):
314+ """
315+ Create a file with expansions that can be used to bypass compile.
316+
317+ If for any reason bypass compile is false, we do not write out the expansion. Only if we
318+ determine to bypass compile do we write out the expansions.
319+ \f
320+
321+ :param project: The evergreen project.
322+ :param build_variant: The build variant whose artifacts we want to use.
323+ :param revision: Base revision of the build.
324+ :param patch_file: A list of all files modified in patch build.
325+ :param out_file: File to write expansions to.
326+ :param json_artifact: The JSON file to write out the metadata of files to attach to task.
341327 """
342- args = parse_args ()
343328 logging .basicConfig (
344329 format = "[%(asctime)s - %(name)s - %(levelname)s] %(message)s" ,
345330 level = logging .DEBUG ,
346331 stream = sys .stdout ,
347332 )
348333
349334 # Determine if we should bypass compile based on modified patch files.
350- if should_bypass_compile (args ):
351- evg_config = read_evg_config ()
352- if evg_config is None :
353- LOGGER .warning (
354- "Could not find ~/.evergreen.yml config file. Default compile bypass to false." )
355- return
356-
357- api_server = "{url.scheme}://{url.netloc}" .format (
358- url = urlparse (evg_config .get ("api_server_host" )))
359- revision_url = "{}/rest/v1/projects/{}/revisions/{}" .format (api_server , args .project ,
360- args .revision )
361- revisions = requests_get_json (revision_url )
362- build_id = find_suitable_build_id (revisions ["builds" ], args )
335+ if should_bypass_compile (patch_file , build_variant ):
336+ evergreen_api = RetryingEvergreenApi .get_api (config_file = EVG_CONFIG_FILE )
337+ build_id = find_build_for_previous_compile_task (evergreen_api , revision , project ,
338+ build_variant )
363339 if not build_id :
364340 LOGGER .warning ("Could not find build id. Default compile bypass to false." ,
365- revision = args . revision , project = args . project )
341+ revision = revision , project = project )
366342 return
367-
368- # Generate the compile task id.
369- index = build_id .find (args .revision )
370- compile_task_id = "{}compile_{}" .format (build_id [:index ], build_id [index :])
371- task_url = "{}/rest/v1/tasks/{}" .format (api_server , compile_task_id )
372- # Get info on compile task of base commit.
373- task = requests_get_json (task_url )
374- if task is None or task ["status" ] != "success" :
343+ task = find_previous_compile_task (evergreen_api , build_id , revision )
344+ if task is None or not task .is_success ():
375345 LOGGER .warning (
376346 "Could not retrieve artifacts because the compile task for base commit"
377- " was not available. Default compile bypass to false." , task_id = compile_task_id )
347+ " was not available. Default compile bypass to false." , task_id = task . task_id )
378348 return
379-
380- # Get the compile task artifacts from REST API
381- LOGGER .info ("Fetching pre-existing artifacts from compile task" , task_id = compile_task_id )
349+ LOGGER .info ("Fetching pre-existing artifacts from compile task" , task_id = task .task_id )
382350 artifacts = []
383- for artifact in task [ "files" ] :
384- filename = os .path .basename (artifact [ " url" ] )
351+ for artifact in task . artifacts :
352+ filename = os .path .basename (artifact . url )
385353 if filename .startswith (build_id ):
386354 LOGGER .info ("Retrieving archive" , filename = filename )
387355 # This is the artifacts.tgz as referenced in evergreen.yml.
388356 try :
389- urllib .request .urlretrieve (artifact [ " url" ] , filename )
357+ urllib .request .urlretrieve (artifact . url , filename )
390358 except urllib .error .ContentTooShortError :
391359 LOGGER .warning (
392360 "The artifact could not be completely downloaded. Default"
@@ -415,7 +383,7 @@ def main(): # pylint: disable=too-many-locals,too-many-statements
415383 LOGGER .info ("Retrieving mongo source" , filename = filename )
416384 # This is the distsrc.[tgz|zip] as referenced in evergreen.yml.
417385 try :
418- urllib .request .urlretrieve (artifact [ " url" ] , filename )
386+ urllib .request .urlretrieve (artifact . url , filename )
419387 except urllib .error .ContentTooShortError :
420388 LOGGER .warn (
421389 "The artifact could not be completely downloaded. Default"
@@ -429,12 +397,12 @@ def main(): # pylint: disable=too-many-locals,too-many-statements
429397 LOGGER .info ("Linking base artifact to this patch build" , filename = filename )
430398 # For other artifacts we just add their URLs to the JSON file to upload.
431399 files = {
432- "name" : artifact [ " name" ] ,
433- "link" : artifact [ " url" ] ,
400+ "name" : artifact . name ,
401+ "link" : artifact . url ,
434402 "visibility" : "private" ,
435403 }
436404 # Check the link exists, else raise an exception. Compile bypass is disabled.
437- requests .head (artifact [ " url" ] ).raise_for_status ()
405+ requests .head (artifact . url ).raise_for_status ()
438406 artifacts .append (files )
439407
440408 # SERVER-21492 related issue where without running scons the jstests/libs/key1
@@ -445,13 +413,12 @@ def main(): # pylint: disable=too-many-locals,too-many-statements
445413 os .chmod ("jstests/libs/keyForRollover" , 0o600 )
446414
447415 # This is the artifacts.json file.
448- write_out_artifacts (args . jsonArtifact , artifacts )
416+ write_out_artifacts (json_artifact , artifacts )
449417
450418 # Need to apply these expansions for bypassing SCons.
451- expansions = generate_bypass_expansions (args .project , args .buildVariant , args .revision ,
452- build_id )
453- write_out_bypass_compile_expansions (args .outFile , ** expansions )
419+ expansions = generate_bypass_expansions (project , build_variant , revision , build_id )
420+ write_out_bypass_compile_expansions (out_file , ** expansions )
454421
455422
456423if __name__ == "__main__" :
457- main ()
424+ main () # pylint: disable=no-value-for-parameter
0 commit comments