def test_get_dep_names_of_package():
    # TEST 1 from external ref:
    # Check that colorama is returned without the install condition when
    # just getting the names (it has a "; ..." conditional originally):
    dep_names = get_dep_names_of_package("python-for-android")
    assert "colorama" in dep_names
    assert "setuptools" not in dep_names
    dep_names = get_dep_names_of_package("python-for-android",
                                         include_build_requirements=True)
    assert "setuptools" in dep_names

    # TEST 2 from local folder:
    assert "colorama" in get_dep_names_of_package(local_repo_folder())

    # Now test that exact version pins are kept, but others aren't:
    test_fake_package = tempfile.mkdtemp()
    try:
        with open(os.path.join(test_fake_package, "setup.py"), "w") as f:
            f.write(
                textwrap.dedent("""\
            from setuptools import setup

            setup(name='fakeproject',
                  description='fake for testing',
                  install_requires=['buildozer==0.39',
                                    'python-for-android>=0.5.1'],
            )
            """))
        # See that we get the deps with the exact version pin kept but
        # the other version restriction gone:
        assert set(
            get_dep_names_of_package(
                test_fake_package,
                recursive=False,
                keep_version_pins=True,
                verbose=True)) == {"buildozer==0.39", "python-for-android"}

        # Make sure we also can get the fully cleaned up variant:
        assert set(
            get_dep_names_of_package(
                test_fake_package,
                recursive=False,
                keep_version_pins=False,
                verbose=True)) == {"buildozer", "python-for-android"}

        # Test with build requirements included:
        dep_names = get_dep_names_of_package(test_fake_package,
                                             recursive=False,
                                             keep_version_pins=False,
                                             verbose=True,
                                             include_build_requirements=True)
        assert len({
            "buildozer", "python-for-android", "setuptools"
        }.intersection(dep_names)) == 3  # all three must be included
    finally:
        shutil.rmtree(test_fake_package)
def test_get_dep_names_of_package():
    # TEST 1 from external ref:
    # Check that colorama is returned without the install condition when
    # just getting the names (it has a "; ..." conditional originally):
    dep_names = get_dep_names_of_package("python-for-android")
    assert "colorama" in dep_names
    assert "setuptools" not in dep_names
    dep_names = get_dep_names_of_package("python-for-android",
                                         include_build_requirements=True)
    assert "setuptools" in dep_names

    # TEST 2 from local folder:
    assert "colorama" in get_dep_names_of_package(local_repo_folder())

    # Now test that exact version pins are kept, but others aren't:
    test_fake_package = tempfile.mkdtemp()
    try:
        with open(os.path.join(test_fake_package, "setup.py"), "w") as f:
            f.write(textwrap.dedent("""\
            from setuptools import setup

            setup(name='fakeproject',
                  description='fake for testing',
                  install_requires=['buildozer==0.39',
                                    'python-for-android>=0.5.1'],
            )
            """))
        # See that we get the deps with the exact version pin kept but
        # the other version restriction gone:
        assert set(get_dep_names_of_package(
            test_fake_package, recursive=False,
            keep_version_pins=True, verbose=True
        )) == {"buildozer==0.39", "python-for-android"}

        # Make sure we also can get the fully cleaned up variant:
        assert set(get_dep_names_of_package(
            test_fake_package, recursive=False,
            keep_version_pins=False, verbose=True
        )) == {"buildozer", "python-for-android"}

        # Test with build requirements included:
        dep_names = get_dep_names_of_package(
            test_fake_package, recursive=False,
            keep_version_pins=False, verbose=True,
            include_build_requirements=True
            )
        assert len(
            {"buildozer", "python-for-android", "setuptools"}.intersection(
                dep_names
            )
        ) == 3  # all three must be included
    finally:
        shutil.rmtree(test_fake_package)
Example #3
0
    def __init__(self):

        argv = sys.argv
        self.warn_on_carriage_return_args(argv)
        # Buildozer used to pass these arguments in a now-invalid order
        # If that happens, apply this fix
        # This fix will be removed once a fixed buildozer is released
        if (len(argv) > 2
                and argv[1].startswith('--color')
                and argv[2].startswith('--storage-dir')):
            argv.append(argv.pop(1))  # the --color arg
            argv.append(argv.pop(1))  # the --storage-dir arg

        parser = NoAbbrevParser(
            description='A packaging tool for turning Python scripts and apps '
                        'into Android APKs')

        generic_parser = argparse.ArgumentParser(
            add_help=False,
            description='Generic arguments applied to all commands')
        argparse.ArgumentParser(
            add_help=False, description='Arguments for dist building')

        generic_parser.add_argument(
            '--debug', dest='debug', action='store_true', default=False,
            help='Display debug output and all build info')
        generic_parser.add_argument(
            '--color', dest='color', choices=['always', 'never', 'auto'],
            help='Enable or disable color output (default enabled on tty)')
        generic_parser.add_argument(
            '--sdk-dir', '--sdk_dir', dest='sdk_dir', default='',
            help='The filepath where the Android SDK is installed')
        generic_parser.add_argument(
            '--ndk-dir', '--ndk_dir', dest='ndk_dir', default='',
            help='The filepath where the Android NDK is installed')
        generic_parser.add_argument(
            '--android-api',
            '--android_api',
            dest='android_api',
            default=0,
            type=int,
            help=('The Android API level to build against defaults to {} if '
                  'not specified.').format(RECOMMENDED_TARGET_API))
        generic_parser.add_argument(
            '--ndk-version', '--ndk_version', dest='ndk_version', default=None,
            help=('DEPRECATED: the NDK version is now found automatically or '
                  'not at all.'))
        generic_parser.add_argument(
            '--ndk-api', type=int, default=None,
            help=('The Android API level to compile against. This should be your '
                  '*minimal supported* API, not normally the same as your --android-api. '
                  'Defaults to min(ANDROID_API, {}) if not specified.').format(RECOMMENDED_NDK_API))
        generic_parser.add_argument(
            '--symlink-java-src', '--symlink_java_src',
            action='store_true',
            dest='symlink_java_src',
            default=False,
            help=('If True, symlinks the java src folder during build and dist '
                  'creation. This is useful for development only, it could also'
                  ' cause weird problems.'))

        default_storage_dir = user_data_dir('python-for-android')
        if ' ' in default_storage_dir:
            default_storage_dir = '~/.python-for-android'
        generic_parser.add_argument(
            '--storage-dir', dest='storage_dir', default=default_storage_dir,
            help=('Primary storage directory for downloads and builds '
                  '(default: {})'.format(default_storage_dir)))

        generic_parser.add_argument(
            '--arch', help='The arch to build for.',
            default='armeabi-v7a')

        # Options for specifying the Distribution
        generic_parser.add_argument(
            '--dist-name', '--dist_name',
            help='The name of the distribution to use or create', default='')

        generic_parser.add_argument(
            '--requirements',
            help=('Dependencies of your app, should be recipe names or '
                  'Python modules. NOT NECESSARY if you are using '
                  'Python 3 with --use-setup-py'),
            default='')

        generic_parser.add_argument(
            '--recipe-blacklist',
            help=('Blacklist an internal recipe from use. Allows '
                  'disabling Python 3 core modules to save size'),
            dest="recipe_blacklist",
            default='')

        generic_parser.add_argument(
            '--blacklist-requirements',
            help=('Blacklist an internal recipe from use. Allows '
                  'disabling Python 3 core modules to save size'),
            dest="blacklist_requirements",
            default='')

        generic_parser.add_argument(
            '--bootstrap',
            help='The bootstrap to build with. Leave unset to choose '
                 'automatically.',
            default=None)

        generic_parser.add_argument(
            '--hook',
            help='Filename to a module that contains python-for-android hooks',
            default=None)

        add_boolean_option(
            generic_parser, ["force-build"],
            default=False,
            description='Whether to force compilation of a new distribution')

        add_boolean_option(
            generic_parser, ["require-perfect-match"],
            default=False,
            description=('Whether the dist recipes must perfectly match '
                         'those requested'))

        add_boolean_option(
            generic_parser, ["allow-replace-dist"],
            default=True,
            description='Whether existing dist names can be automatically replaced'
            )

        generic_parser.add_argument(
            '--local-recipes', '--local_recipes',
            dest='local_recipes', default='./p4a-recipes',
            help='Directory to look for local recipes')

        generic_parser.add_argument(
            '--java-build-tool',
            dest='java_build_tool', default='auto',
            choices=['auto', 'ant', 'gradle'],
            help=('The java build tool to use when packaging the APK, defaults '
                  'to automatically selecting an appropriate tool.'))

        add_boolean_option(
            generic_parser, ['copy-libs'],
            default=False,
            description='Copy libraries instead of using biglink (Android 4.3+)'
        )

        self._read_configuration()

        subparsers = parser.add_subparsers(dest='subparser_name',
                                           help='The command to run')

        def add_parser(subparsers, *args, **kwargs):
            """
            argparse in python2 doesn't support the aliases option,
            so we just don't provide the aliases there.
            """
            if 'aliases' in kwargs and sys.version_info.major < 3:
                kwargs.pop('aliases')
            return subparsers.add_parser(*args, **kwargs)

        add_parser(
            subparsers,
            'recommendations',
            parents=[generic_parser],
            help='List recommended p4a dependencies')
        parser_recipes = add_parser(
            subparsers,
            'recipes',
            parents=[generic_parser],
            help='List the available recipes')
        parser_recipes.add_argument(
            "--compact",
            action="store_true", default=False,
            help="Produce a compact list suitable for scripting")
        add_parser(
            subparsers, 'bootstraps',
            help='List the available bootstraps',
            parents=[generic_parser])
        add_parser(
            subparsers, 'clean_all',
            aliases=['clean-all'],
            help='Delete all builds, dists and caches',
            parents=[generic_parser])
        add_parser(
            subparsers, 'clean_dists',
            aliases=['clean-dists'],
            help='Delete all dists',
            parents=[generic_parser])
        add_parser(
            subparsers, 'clean_bootstrap_builds',
            aliases=['clean-bootstrap-builds'],
            help='Delete all bootstrap builds',
            parents=[generic_parser])
        add_parser(
            subparsers, 'clean_builds',
            aliases=['clean-builds'],
            help='Delete all builds',
            parents=[generic_parser])

        parser_clean = add_parser(
            subparsers, 'clean',
            help='Delete build components.',
            parents=[generic_parser])
        parser_clean.add_argument(
            'component', nargs='+',
            help=('The build component(s) to delete. You can pass any '
                  'number of arguments from "all", "builds", "dists", '
                  '"distributions", "bootstrap_builds", "downloads".'))

        parser_clean_recipe_build = add_parser(
            subparsers,
            'clean_recipe_build', aliases=['clean-recipe-build'],
            help=('Delete the build components of the given recipe. '
                  'By default this will also delete built dists'),
            parents=[generic_parser])
        parser_clean_recipe_build.add_argument(
            'recipe', help='The recipe name')
        parser_clean_recipe_build.add_argument(
            '--no-clean-dists', default=False,
            dest='no_clean_dists',
            action='store_true',
            help='If passed, do not delete existing dists')

        parser_clean_download_cache = add_parser(
            subparsers,
            'clean_download_cache', aliases=['clean-download-cache'],
            help='Delete cached downloads for requirement builds',
            parents=[generic_parser])
        parser_clean_download_cache.add_argument(
            'recipes',
            nargs='*',
            help='The recipes to clean (space-separated). If no recipe name is'
                  ' provided, the entire cache is cleared.')

        parser_export_dist = add_parser(
            subparsers,
            'export_dist', aliases=['export-dist'],
            help='Copy the named dist to the given path',
            parents=[generic_parser])
        parser_export_dist.add_argument('output_dir',
                                        help='The output dir to copy to')
        parser_export_dist.add_argument(
            '--symlink',
            action='store_true',
            help='Symlink the dist instead of copying')

        parser_apk = add_parser(
            subparsers,
            'apk', help='Build an APK',
            parents=[generic_parser])
        # This is actually an internal argument of the build.py
        # (see pythonforandroid/bootstraps/common/build/build.py).
        # However, it is also needed before the distribution is finally
        # assembled for locating the setup.py / other build systems, which
        # is why we also add it here:
        parser_apk.add_argument(
            '--private', dest='private',
            help='the directory with the app source code files' +
                 ' (containing your main.py entrypoint)',
            required=False, default=None)
        parser_apk.add_argument(
            '--release', dest='build_mode', action='store_const',
            const='release', default='debug',
            help='Build the PARSER_APK. in Release mode')
        parser_apk.add_argument(
            '--use-setup-py', dest="use_setup_py",
            action='store_true', default=False,
            help="Process the setup.py of a project if present. " +
                 "(Experimental!")
        parser_apk.add_argument(
            '--ignore-setup-py', dest="ignore_setup_py",
            action='store_true', default=False,
            help="Don't run the setup.py of a project if present. " +
                 "This may be required if the setup.py is not " +
                 "designed to work inside p4a (e.g. by installing " +
                 "dependencies that won't work or aren't desired " +
                 "on Android")
        parser_apk.add_argument(
            '--keystore', dest='keystore', action='store', default=None,
            help=('Keystore for JAR signing key, will use jarsigner '
                  'default if not specified (release build only)'))
        parser_apk.add_argument(
            '--signkey', dest='signkey', action='store', default=None,
            help='Key alias to sign PARSER_APK. with (release build only)')
        parser_apk.add_argument(
            '--keystorepw', dest='keystorepw', action='store', default=None,
            help='Password for keystore')
        parser_apk.add_argument(
            '--signkeypw', dest='signkeypw', action='store', default=None,
            help='Password for key alias')

        add_parser(
            subparsers,
            'create', help='Compile a set of requirements into a dist',
            parents=[generic_parser])
        add_parser(
            subparsers,
            'archs', help='List the available target architectures',
            parents=[generic_parser])
        add_parser(
            subparsers,
            'distributions', aliases=['dists'],
            help='List the currently available (compiled) dists',
            parents=[generic_parser])
        add_parser(
            subparsers,
            'delete_dist', aliases=['delete-dist'], help='Delete a compiled dist',
            parents=[generic_parser])

        parser_sdk_tools = add_parser(
            subparsers,
            'sdk_tools', aliases=['sdk-tools'],
            help='Run the given binary from the SDK tools dis',
            parents=[generic_parser])
        parser_sdk_tools.add_argument(
            'tool', help='The binary tool name to run')

        add_parser(
            subparsers,
            'adb', help='Run adb from the given SDK',
            parents=[generic_parser])
        add_parser(
            subparsers,
            'logcat', help='Run logcat from the given SDK',
            parents=[generic_parser])
        add_parser(
            subparsers,
            'build_status', aliases=['build-status'],
            help='Print some debug information about current built components',
            parents=[generic_parser])

        parser.add_argument('-v', '--version', action='version',
                            version=__version__)

        args, unknown = parser.parse_known_args(sys.argv[1:])
        args.unknown_args = unknown

        if hasattr(args, "private") and args.private is not None:
            # Pass this value on to the internal bootstrap build.py:
            args.unknown_args += ["--private", args.private]
        if hasattr(args, "ignore_setup_py") and args.ignore_setup_py:
            args.use_setup_py = False

        self.args = args

        if args.subparser_name is None:
            parser.print_help()
            exit(1)

        setup_color(args.color)

        if args.debug:
            logger.setLevel(logging.DEBUG)

        self.ctx = Context()
        self.ctx.use_setup_py = getattr(args, "use_setup_py", True)

        have_setup_py_or_similar = False
        if getattr(args, "private", None) is not None:
            project_dir = getattr(args, "private")
            if (os.path.exists(os.path.join(project_dir, "setup.py")) or
                    os.path.exists(os.path.join(project_dir,
                                                "pyproject.toml"))):
                have_setup_py_or_similar = True

        # Process requirements and put version in environ
        if hasattr(args, 'requirements'):
            requirements = []

            # Add dependencies from setup.py, but only if they are recipes
            # (because otherwise, setup.py itself will install them later)
            if (have_setup_py_or_similar and
                    getattr(args, "use_setup_py", False)):
                try:
                    info("Analyzing package dependencies. MAY TAKE A WHILE.")
                    # Get all the dependencies corresponding to a recipe:
                    dependencies = [
                        dep.lower() for dep in
                        get_dep_names_of_package(
                            args.private,
                            keep_version_pins=True,
                            recursive=True,
                            verbose=True,
                        )
                    ]
                    info("Dependencies obtained: " + str(dependencies))
                    all_recipes = [
                        recipe.lower() for recipe in
                        set(Recipe.list_recipes(self.ctx))
                    ]
                    dependencies = set(dependencies).intersection(
                        set(all_recipes)
                    )
                    # Add dependencies to argument list:
                    if len(dependencies) > 0:
                        if len(args.requirements) > 0:
                            args.requirements += u","
                        args.requirements += u",".join(dependencies)
                except ValueError:
                    # Not a python package, apparently.
                    warning(
                        "Processing failed, is this project a valid "
                        "package? Will continue WITHOUT setup.py deps."
                    )

            # Parse --requirements argument list:
            for requirement in split_argument_list(args.requirements):
                if "==" in requirement:
                    requirement, version = requirement.split(u"==", 1)
                    os.environ["VERSION_{}".format(requirement)] = version
                    info('Recipe {}: version "{}" requested'.format(
                        requirement, version))
                requirements.append(requirement)
            args.requirements = u",".join(requirements)

        self.warn_on_deprecated_args(args)

        self.storage_dir = args.storage_dir
        self.ctx.setup_dirs(self.storage_dir)
        self.sdk_dir = args.sdk_dir
        self.ndk_dir = args.ndk_dir
        self.android_api = args.android_api
        self.ndk_api = args.ndk_api
        self.ctx.symlink_java_src = args.symlink_java_src
        self.ctx.java_build_tool = args.java_build_tool

        self._archs = split_argument_list(args.arch)

        self.ctx.local_recipes = args.local_recipes
        self.ctx.copy_libs = args.copy_libs

        # Each subparser corresponds to a method
        getattr(self, args.subparser_name.replace('-', '_'))(args)
Example #4
0
def test_get_dep_names_of_package():
    # TEST 1 from external ref:
    # Check that colorama is returned without the install condition when
    # just getting the names (it has a "; ..." conditional originally):
    dep_names = get_dep_names_of_package("python-for-android")
    assert "colorama" in dep_names
    assert "setuptools" not in dep_names
    try:
        dep_names = get_dep_names_of_package(
            "python-for-android", include_build_requirements=True,
            verbose=True,
        )
    except NotImplementedError as e:
        # If python-for-android was fetched as wheel then build requirements
        # cannot be obtained (since that is not implemented for wheels).
        # Check for the correct error message:
        assert "wheel" in str(e)
        # (And yes it would be better to do a local test with something
        #  that is guaranteed to be a wheel and not remote on pypi,
        #  but that might require setting up a full local pypiserver.
        #  Not worth the test complexity for now, but if anyone has an
        #  idea in the future feel free to replace this subtest.)
    else:
        # We managed to obtain build requirements!
        # Check setuptools is in here:
        assert "setuptools" in dep_names

    # TEST 2 from local folder:
    assert "colorama" in get_dep_names_of_package(local_repo_folder())

    # Now test that exact version pins are kept, but others aren't:
    test_fake_package = tempfile.mkdtemp()
    try:
        with open(os.path.join(test_fake_package, "setup.py"), "w") as f:
            f.write(textwrap.dedent("""\
            from setuptools import setup

            setup(name='fakeproject',
                  description='fake for testing',
                  install_requires=['buildozer==0.39',
                                    'python-for-android>=0.5.1'],
            )
            """))
        # See that we get the deps with the exact version pin kept but
        # the other version restriction gone:
        assert set(get_dep_names_of_package(
            test_fake_package, recursive=False,
            keep_version_pins=True, verbose=True
        )) == {"buildozer==0.39", "python-for-android"}

        # Make sure we also can get the fully cleaned up variant:
        assert set(get_dep_names_of_package(
            test_fake_package, recursive=False,
            keep_version_pins=False, verbose=True
        )) == {"buildozer", "python-for-android"}

        # Test with build requirements included:
        dep_names = get_dep_names_of_package(
            test_fake_package, recursive=False,
            keep_version_pins=False, verbose=True,
            include_build_requirements=True
            )
        assert len(
            {"buildozer", "python-for-android", "setuptools"}.intersection(
                dep_names
            )
        ) == 3  # all three must be included
    finally:
        shutil.rmtree(test_fake_package)