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))
Example #2
0
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)
Example #3
0
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))
Example #5
0
 def write(self, path):
     """Write the recipe to disk."""
     FoundationPlist.writePlist(self["keys"], path)
Example #6
0
def write_report(report, report_file):
    FoundationPlist.writePlist(report, report_file)
Example #7
0
def write_report(report, report_file):
    FoundationPlist.writePlist(report, report_file)
Example #8
0
 def write(self, path):
     """Write the recipe to disk."""
     FoundationPlist.writePlist(self["keys"], path)
Example #9
0
    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))
Example #10
0
    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)