def test_sparkle_feed_app(self): # TODO (Shea): Mock up an "app" for testing purposes. # TODO (Shea): Add arguments to only produce certain RecipeTypes. # This will allow us to narrow the tests down. prefs = FoundationPlist.readPlist(os.path.expanduser( "~/Library/Preferences/com.elliotjordan.recipe-robot.plist")) # Robby needs a recipe for Skitch. He decides to try the Robot! app = "Evernote" destination = get_output_path(prefs, app) clean_folder(destination) subprocess.check_call( ["./recipe-robot", "--ignore-existing", "/Applications/%s.app" % app]) # First, test the download recipe. # We know that (Shea's non-mocked "real" copy) Evernote has a # Sparkle Feed. Test to ensure download recipe uses it. download_recipe_path = get_output_path(prefs, app, recipe_type="download") download_recipe = FoundationPlist.readPlist(download_recipe_path) assert_in("Process", download_recipe) assert_equals("https://update.evernote.com/public/ENMacSMD/EvernoteMacUpdate.xml", download_recipe["Input"]["SPARKLE_FEED_URL"]) assert_in("URLDownloader", [processor["Processor"] for processor in download_recipe["Process"]]) url_downloader = [processor for processor in download_recipe["Process"] if processor["Processor"] == "URLDownloader"][0] args = url_downloader["Arguments"] assert_dict_equal({"filename": "%NAME%-%version%.zip"}, dict(args))
def generate_recipes(facts, prefs): """Generate the selected types of recipes. Args: facts: A continually-updated dictionary containing all the information we know so far about the app associated with the input path. prefs: The dictionary containing a key/value pair for each preference. """ recipes = facts["recipes"] if "app_name" in facts: if not facts["args"].ignore_existing: create_existing_recipe_list(facts) else: raise RoboError("I wasn't able to determine the name of this app, so I " "can't make any recipes.") preferred = [recipe for recipe in recipes if recipe["preferred"]] raise_if_recipes_cannot_be_generated(facts, preferred) # We have enough information to create a recipe set, but with assumptions. # TODO(Elliot): This code may not be necessary if inspections do their job. if "codesign_reqs" not in facts and "codesign_authorities" not in facts: facts["reminders"].append( "I can't tell whether this app is codesigned or not, so I'm " "going to assume it's not. You may want to verify that yourself " "and add the CodeSignatureVerifier processor if necessary." ) facts["codesign_reqs"] = "" facts["codesign_authorities"] = [] if "version_key" not in facts: facts["reminders"].append( "I can't tell whether to use CFBundleShortVersionString or " "CFBundleVersion for the version key of this app. Most apps use " "CFBundleShortVersionString, so that's what I'll use. You may " "want to verify that and modify the recipes if necessary." ) facts["version_key"] = "CFBundleShortVersionString" # TODO(Elliot): Run `autopkg repo-list` once and store the resulting value # for future use when detecting missing required repos, rather than running # `autopkg repo-list` separately during each check. (For example, the # FileWaveImporter repo must be present to run created filewave recipes.) # Prepare the destination directory. # TODO (Shea): This JSS Recipe format code is repeated all over. # Smells like a refactor. if "developer" in facts and prefs.get("FollowOfficialJSSRecipesFormat", False) is not True: recipe_dest_dir = robo_join(prefs["RecipeCreateLocation"], facts["developer"].replace("/", "-")) else: recipe_dest_dir = robo_join(prefs["RecipeCreateLocation"], facts["app_name"].replace("/", "-")) facts["recipe_dest_dir"] = recipe_dest_dir create_dest_dirs(recipe_dest_dir) build_recipes(facts, preferred, prefs) # TODO (Shea): As far as I can tell, the only pref that changes is the # recipe created count. Move out from here! # Save preferences to disk for next time. FoundationPlist.writePlist(prefs, PREFS_FILE)
def test(): """Functional tests""" # Read preferences. prefs = FoundationPlist.readPlist( os.path.expanduser( "~/Library/Preferences/com.elliotjordan.recipe-robot.plist")) shuffle(SAMPLE_DATA) for app in SAMPLE_DATA: # Remove output folder, if it exists. destination = get_output_path(prefs, app["app_name"], app["developer"]) clean_folder(destination) yield robot_runner, app["input_path"], app["app_name"], app[ "developer"] recipes = {} for recipe_type in RECIPE_TYPES: recipe_path = get_output_path(prefs, app["app_name"], app["developer"], recipe_type=recipe_type) if recipe_type in ("download", "pkg"): # TODO: Remove AutoPkg cache folder, if it exists. if os.path.isfile(recipe_path): yield autopkg_runner, recipe_path
def test_sparkle_feed_app(self): # TODO (Shea): Mock up an "app" for testing purposes. # TODO (Shea): Add arguments to only produce certain RecipeTypes. # This will allow us to narrow the tests down. prefs = FoundationPlist.readPlist( os.path.expanduser( "~/Library/Preferences/com.elliotjordan.recipe-robot.plist")) # Robby needs a recipe for Skitch. He decides to try the Robot! app = "Evernote" destination = get_output_path(prefs, app) clean_folder(destination) subprocess.check_call([ "./recipe-robot", "--ignore-existing", "/Applications/%s.app" % app ]) # First, test the download recipe. # We know that (Shea's non-mocked "real" copy) Evernote has a # Sparkle Feed. Test to ensure download recipe uses it. download_recipe_path = get_output_path(prefs, app, recipe_type="download") download_recipe = FoundationPlist.readPlist(download_recipe_path) assert_in("Process", download_recipe) assert_equals( "https://update.evernote.com/public/ENMacSMD/EvernoteMacUpdate.xml", download_recipe["Input"]["SPARKLE_FEED_URL"]) assert_in("URLDownloader", [ processor["Processor"] for processor in download_recipe["Process"] ]) url_downloader = [ processor for processor in download_recipe["Process"] if processor["Processor"] == "URLDownloader" ][0] args = url_downloader["Arguments"] assert_dict_equal({"filename": "%NAME%-%version%.zip"}, dict(args))
def write(self, path): """Write the recipe to disk.""" FoundationPlist.writePlist(self["keys"], path)
def write_report(report, report_file): FoundationPlist.writePlist(report, report_file)
def test(self): prefs = FoundationPlist.readPlist( os.path.expanduser( "~/Library/Preferences/com.elliotjordan.recipe-robot.plist")) # Robby needs a recipe for Evernote. He decides to try the Robot! app = "Evernote" developer = "Evernote" destination = get_output_path(prefs, app, developer) clean_folder(destination) subprocess.check_call([ "./recipe-robot", "--ignore-existing", "--verbose", "/Applications/%s.app" % app, ]) # Ensure the download recipe uses the known-good Sparkle feed URL. download_recipe_path = get_output_path(prefs, app, developer, recipe_type="download") download_recipe = FoundationPlist.readPlist(download_recipe_path) assert_in("Process", download_recipe) assert_equals( "https://update.evernote.com/public/ENMacSMD/EvernoteMacUpdate.xml", download_recipe["Input"]["SPARKLE_FEED_URL"], ) # Make sure URLDownloader is present and uses the expected filename. assert_in( "URLDownloader", [ processor["Processor"] for processor in download_recipe["Process"] ], ) urldownloader_args = [ processor for processor in download_recipe["Process"] if processor["Processor"] == "URLDownloader" ][0]["Arguments"] expected_args = {"filename": "%NAME%-%version%.zip"} assert_dict_equal(expected_args, dict(urldownloader_args)) # Make sure EndOfCheckPhase is present. assert_in( "EndOfCheckPhase", [ processor["Processor"] for processor in download_recipe["Process"] ], ) # Make sure Unarchiver is present with expected arguments. assert_in( "Unarchiver", [ processor["Processor"] for processor in download_recipe["Process"] ], ) unarchiver_args = [ processor for processor in download_recipe["Process"] if processor["Processor"] == "Unarchiver" ][0]["Arguments"] expected_args = { "destination_path": "%RECIPE_CACHE_DIR%/%NAME%", "archive_path": "%pathname%", "purge_destination": True, } assert_dict_equal(expected_args, dict(unarchiver_args)) # Make sure CodeSignatureVerifier is present with expected arguments. assert_in( "CodeSignatureVerifier", [ processor["Processor"] for processor in download_recipe["Process"] ], ) codesigverifier_args = [ processor for processor in download_recipe["Process"] if processor["Processor"] == "CodeSignatureVerifier" ][0]["Arguments"] expected_args = { "input_path": "%RECIPE_CACHE_DIR%/%NAME%/Evernote.app", "requirement": ('identifier "com.evernote.Evernote" and ' "anchor apple generic and " "certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and " "certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and " "certificate leaf[subject.OU] = Q79WDW8YH9"), } assert_dict_equal(expected_args, dict(codesigverifier_args))
def test(self): prefs = FoundationPlist.readPlist( os.path.expanduser( "~/Library/Preferences/com.elliotjordan.recipe-robot.plist")) # Robby loves MunkiAdmin. Let's make some recipes. app = "MunkiAdmin" developer = "Hannes Juutilainen" url = "https://github.com/hjuutilainen/munkiadmin" destination = get_output_path(prefs, app, developer) clean_folder(destination) subprocess.check_call( ["./recipe-robot", "--ignore-existing", "--verbose", url]) # Ensure the download recipe uses the correct GitHub project. download_recipe_path = get_output_path(prefs, app, developer, recipe_type="download") download_recipe = FoundationPlist.readPlist(download_recipe_path) assert_in("Process", download_recipe) assert_equals("hjuutilainen/munkiadmin", download_recipe["Input"]["GITHUB_REPO"]) # Make sure GitHubReleasesInfoProvider is present and uses the correct repo. assert_in( "GitHubReleasesInfoProvider", [ processor["Processor"] for processor in download_recipe["Process"] ], ) githubreleasesinfoprovider_args = [ processor for processor in download_recipe["Process"] if processor["Processor"] == "GitHubReleasesInfoProvider" ][0]["Arguments"] expected_args = {"github_repo": "%GITHUB_REPO%"} assert_dict_equal(expected_args, dict(githubreleasesinfoprovider_args)) # Make sure URLDownloader is present and has the expected filename. assert_in( "URLDownloader", [ processor["Processor"] for processor in download_recipe["Process"] ], ) urldownloader_args = [ processor for processor in download_recipe["Process"] if processor["Processor"] == "URLDownloader" ][0]["Arguments"] expected_args = {"filename": "%NAME%-%version%.dmg"} assert_dict_equal(expected_args, dict(urldownloader_args)) # Make sure EndOfCheckPhase is present. assert_in( "EndOfCheckPhase", [ processor["Processor"] for processor in download_recipe["Process"] ], ) # Make sure CodeSignatureVerifier is present with expected arguments. assert_in( "CodeSignatureVerifier", [ processor["Processor"] for processor in download_recipe["Process"] ], ) codesigverifier_args = [ processor for processor in download_recipe["Process"] if processor["Processor"] == "CodeSignatureVerifier" ][0]["Arguments"] expected_args = { "input_path": "%pathname%/MunkiAdmin.app", "requirement": ('anchor apple generic and identifier "com.hjuutilainen.MunkiAdmin" and ' "(certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or " "certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and " "certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and " 'certificate leaf[subject.OU] = "8XXWJ76X9Y")'), } assert_dict_equal(expected_args, dict(codesigverifier_args)) # Make sure Versioner is present with expected arguments. assert_in( "Versioner", [ processor["Processor"] for processor in download_recipe["Process"] ], ) versioner_args = [ processor for processor in download_recipe["Process"] if processor["Processor"] == "Versioner" ][0]["Arguments"] expected_args = { "input_plist_path": "%pathname%/MunkiAdmin.app/Contents/Info.plist", "plist_version_key": "CFBundleShortVersionString", } assert_dict_equal(expected_args, dict(versioner_args))
def generate_recipes(facts, prefs, recipes): """Generate the selected types of recipes. Args: facts: A continually-updated dictionary containing all the information we know so far about the app associated with the input path. prefs: The dictionary containing a key/value pair for each preference. recipes: The list of known recipe types, created by init_recipes(). """ preferred = [recipe for recipe in recipes if recipe["preferred"]] # No recipe types are preferred. if not preferred: robo_print("Sorry, no recipes available to generate.", LogLevel.ERROR) # We don't have enough information to create a recipe set. if (facts["is_from_app_store"] is False and "sparkle_feed" not in facts and "github_repo" not in facts and "sourceforge_id" not in facts and "download_url" not in facts): robo_print("Sorry, I don't know how to download this app. " "Maybe try another angle? If you provided an app, try " "providing the Sparkle feed for the app instead. Or maybe " "the app's developers offer a direct download URL on their " "website.", LogLevel.ERROR) if (facts["is_from_app_store"] is False and "download_format" not in facts): robo_print("Sorry, I can't tell what format to download this app in. " "Maybe try another angle? If you provided an app, try " "providing the Sparkle feed for the app instead. Or maybe " "the app's developers offer a direct download URL on their " "website.", LogLevel.ERROR) # We have enough information to create a recipe set, but with assumptions. # TODO(Elliot): This code may not be necessary if inspections do their job. if "codesign_reqs" not in facts and "codesign_authorities" not in facts: robo_print("I can't tell whether this app is codesigned or not, so " "I'm going to assume it's not. You may want to verify that " "yourself and add the CodeSignatureVerifier processor if " "necessary.", LogLevel.REMINDER) facts["codesign_reqs"] = "" facts["codesign_authorities"] = [] if "version_key" not in facts: robo_print("I can't tell whether to use CFBundleShortVersionString or " "CFBundleVersion for the version key of this app. Most " "apps use CFBundleShortVersionString, so that's what I'll " "use. You may want to verify that and modify the recipes " "if necessary.", LogLevel.REMINDER) facts["version_key"] = "CFBundleShortVersionString" # TODO(Elliot): Run `autopkg repo-list` once and store the resulting value for # future use when detecting missing required repos, rather than running # `autopkg repo-list` separately during each check. (For example, the # FileWaveImporter repo must be present to run created filewave recipes.) # Prepare the destination directory. if "developer" in facts and prefs.get("FollowOfficialJSSRecipesFormat", False) is not True: recipe_dest_dir = os.path.join(os.path.expanduser(prefs["RecipeCreateLocation"]), facts["developer"].replace("/", "-")) else: recipe_dest_dir = os.path.join(os.path.expanduser(prefs["RecipeCreateLocation"]), facts["app_name"].replace("/", "-")) create_dest_dirs(recipe_dest_dir) # Create a recipe for each preferred type we know about. for recipe in preferred: # TODO (Shea): This could be a global constant. Well, maybe. # Construct the default keys common to all recipes. recipe["keys"] = { "Identifier": "", "MinimumVersion": "0.5.0", "Input": { "NAME": facts["app_name"] }, "Process": [], "Comment": "Created with Recipe Robot v%s " "(https://github.com/homebysix/recipe-robot)" % __version__ } keys = recipe["keys"] # Set the recipe filename (spaces are OK). recipe["filename"] = "%s.%s.recipe" % (facts["app_name"], recipe["type"]) # Set the recipe identifier. keys["Identifier"] = "%s.%s.%s" % (prefs["RecipeIdentifierPrefix"], recipe["type"], facts["app_name"].replace(" ", "")) # If the name of the app bundle differs from the name of the app # itself, we need another input variable for that. if "app_file" in facts: keys["Input"]["APP_FILENAME"] = facts["app_file"] facts["app_name_key"] = "%APP_FILENAME%" else: facts["app_name_key"] = "%NAME%" # Set keys specific to download recipes. if recipe["type"] == "download": generate_download_recipe(facts, prefs, recipe) # Set keys specific to App Store munki overrides. elif recipe["type"] == "munki" and facts["is_from_app_store"] is True: generate_app_store_munki_recipe(facts, prefs, recipe) # Set keys specific to non-App Store munki recipes. elif recipe["type"] == "munki" and facts["is_from_app_store"] is False: generate_munki_recipe(facts, prefs, recipe) # Set keys specific to App Store pkg overrides. elif recipe["type"] == "pkg" and facts["is_from_app_store"] is True: generate_app_store_pkg_recipe(facts, prefs, recipe) # Set keys specific to non-App Store pkg recipes. elif recipe["type"] == "pkg" and facts["is_from_app_store"] is False: generate_pkg_recipe(facts, prefs, recipe) # Set keys specific to install recipes. elif recipe["type"] == "install": generate_install_recipe(facts, prefs, recipe) # Set keys specific to jss recipes. elif recipe["type"] == "jss": generate_jss_recipe(facts, prefs, recipe) # Set keys specific to absolute recipes. elif recipe["type"] == "absolute": generate_absolute_recipe(facts, prefs, recipe) # Set keys specific to sccm recipes. elif recipe["type"] == "sccm": generate_sccm_recipe(facts, prefs, recipe) # Set keys specific to ds recipes. elif recipe["type"] == "ds": generate_ds_recipe(facts, prefs, recipe) # Set keys specific to filewave recipes. elif recipe["type"] == "filewave": generate_filewave_recipe(facts, prefs, recipe) else: # This shouldn't happen, if all the right recipe types are # specified in init_recipes() and also specified above. robo_print("Oops, I think my programmer messed up. I don't " "yet know how to generate a %s recipe. Sorry about " "that." % recipe["type"], LogLevel.WARNING) # Write the recipe to disk. if len(recipe["keys"]["Process"]) > 0: dest_path = os.path.join(recipe_dest_dir, recipe["filename"]) if not os.path.exists(dest_path): # Keep track of the total number of unique recipes we've created. prefs["RecipeCreateCount"] += 1 FoundationPlist.writePlist(recipe["keys"], dest_path) robo_print("%s" % os.path.join(recipe_dest_dir, recipe["filename"]), LogLevel.LOG, 4) facts["recipes"].append(dest_path) # Save preferences to disk for next time. FoundationPlist.writePlist(prefs, prefs_file)