Xcode/AppleURLSearcher.py (149 lines of code) (raw):

#!/usr/bin/python # # Copyright (c) Facebook, Inc. and its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # """See docstring for AppleURLSearcher class""" import datetime import json import os.path import posixpath import re import subprocess from autopkglib import Processor, ProcessorError try: # python 2 from urlparse import urlsplit except ImportError: from urllib.parse import urlsplit __all__ = ["AppleURLSearcher"] class AppleURLSearcher(Processor): """Search the various Apple URLs for a matching Xcode.""" description = __doc__ input_variables = { "result_output_var_name": { "description": ( "The name of the output variable that is returned " "by the match. If not specified then a default of " '"match" will be used.' ), "required": False, "default": "match", }, "re_pattern": { "required": True, "description": ( "Path to download data file from AppleCookieDownloader." "Ignored if BETA is set in the environment." ), }, } output_variables = { "result_output_var_name": { "description": ( "First matched sub-pattern from input found on the fetched " "URL. Note the actual name of variable depends on the input " 'variable "result_output_var_name" or is assigned a default of ' '"match."' ) } } # This code is taken directly from URLTextSearcher def get_url_and_search(self, url, re_pattern, headers=None, flags=None, opts=None): """Get data from url and search for re_pattern""" flag_accumulator = 0 if flags: for flag in flags: if flag in re.__dict__: flag_accumulator += re.__dict__[flag] re_pattern = re.compile(re_pattern, flags=flag_accumulator) try: cmd = [self.env["CURL_PATH"], "--location", "--compressed"] if headers: for header, value in headers.items(): cmd.extend(["--header", "%s: %s" % (header, value)]) if opts: for item in opts: cmd.extend([item]) cmd.append(url) proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (content, stderr) = proc.communicate() if proc.returncode: raise ProcessorError("Could not retrieve URL %s: %s" % (url, stderr)) except OSError: raise ProcessorError("Could not retrieve URL: %s" % url) # Output this to disk so I can search it later with open( os.path.join(self.env["RECIPE_CACHE_DIR"], "downloads", "url_text.txt"), "wb", ) as f: f.write(content) match = re_pattern.search(content.decode('utf-8')) if not match: raise ProcessorError("No match found on URL: %s" % url) # return the last matched group with the dict of named groups return (match.group(match.lastindex or 0), match.groupdict()) def output_result(self, url): """Output the desired result.""" # The final entry is the highest one self.output("Full URL: %s" % url) self.env[self.env["result_output_var_name"]] = url self.output_variables = {} self.output_variables[self.env["result_output_var_name"]] = url def main(self): # If we have "URL" already passed in, we should just use it if self.env.get("URL"): self.output_result(self.env["URL"]) return if self.env.get("BETA"): self.output("Beta flag is set, searching Apple downloads URL...") beta_url = "https://developer.apple.com/download/" # We're going to make the strong assumption that if BETA is # populated, we should only use URLTextSearcher, because as of # 6/7/19, Apple has only posted the new Xcode beta to the main # developer page, and not the "More Downloads" section. # If this trend holds true, then URLTextSearcher = betas, # AppleURLSearcher = "more downloads" = stable/GM releases. # If we do get a url from URLTextSearcher, it needs to be appended # to the base Apple Developer Portal URL. curl_opts = [ "--cookie", "login_cookies", "--cookie-jar", "download_cookies", ] pattern = r"""<a href=["'](.*.xip)""" groupmatch, groupdict = self.get_url_and_search( beta_url, pattern, opts=curl_opts ) fixed_url = "https://developer.apple.com/" + groupmatch self.env[self.env["result_output_var_name"]] = fixed_url self.output("New fixed URL: {}".format(fixed_url)) self.output_variables = {} self.output_variables[self.env["result_output_var_name"]] = fixed_url return self.output("Beta flag not set, searching More downloads list...") # If we're not looking for BETA, then disregard everything from # URLTextSearcher and search the Apple downloads list instead. download_dir = os.path.join(self.env["RECIPE_CACHE_DIR"], "downloads") downloads = os.path.join(download_dir, "listDownloads") if not os.path.exists(downloads): raise ProcessorError("Missing the download data from AppleCookieDownloader") pattern = self.env["re_pattern"] with open(downloads) as f: data = json.load(f) dl_base_url = "https://download.developer.apple.com" xcode_list = [] for x in data["downloads"]: for y in x["files"]: url = dl_base_url + y["remotePath"] # Regex the results re_pattern = re.compile(pattern) dl_match = re_pattern.findall(url) if not dl_match: continue filename = os.path.splitext( posixpath.basename(urlsplit(y["remotePath"]).path) )[0] xcode_item = { "datePublished_str": x["datePublished"], "datePublished_obj": datetime.datetime.strptime( x["datePublished"], "%m/%d/%y %H:%M" ), "remotePath": y["remotePath"], "filename": filename, "full_url": url, } xcode_list.append(xcode_item) matches = sorted(xcode_list, key=lambda i: i["datePublished_obj"]) match = matches[-1] if not match or not xcode_list: raise ProcessorError("No match found!") self.output( "Sorted list of possible filenames: {}".format( [x["filename"] for x in matches] ), verbose_level=2, ) self.output("Found matching item: {}".format(match["filename"])) full_url_match = match["full_url"] if not full_url_match: raise ProcessorError("No matching URL found!") self.output_result(full_url_match) if __name__ == "__main__": PROCESSOR = AppleURLSearcher() PROCESSOR.execute_shell()