├── .deepsource.toml ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── android.yml │ ├── docker.yml │ ├── ios.yml │ ├── no-response.yml │ ├── pypi-release.yml │ ├── support.yml │ └── test_python.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTACT.md ├── CONTRIBUTING.md ├── Dockerfile ├── FAQ.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── buildozer ├── __init__.py ├── __main__.py ├── buildops.py ├── default.spec ├── exceptions.py ├── jsonstore.py ├── libs │ ├── __init__.py │ ├── _structures.py │ └── version.py ├── logger.py ├── scripts │ ├── __init__.py │ ├── client.py │ └── remote.py ├── sitecustomize.py ├── specparser.py ├── target.py ├── targets │ ├── __init__.py │ ├── android.py │ ├── ios.py │ └── osx.py └── tools │ └── packer │ ├── .gitignore │ ├── CHANGELOG │ ├── Makefile │ ├── README.md │ ├── http │ ├── buildozer.desktop │ ├── kivy-icon-96.png │ ├── preseed.cfg │ ├── wallpaper.png │ └── welcome │ │ ├── buildozer.css │ │ ├── index.html │ │ └── milligram.min.css │ ├── launch │ ├── scripts │ ├── additional-packages.sh │ ├── install-virtualbox-guest-additions.sh │ ├── minimize.sh │ └── setup.sh │ └── template.json ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── contact.rst │ ├── contribute.rst │ ├── faq.rst │ ├── index.rst │ ├── installation.rst │ ├── quickstart.rst │ ├── recipes.rst │ └── specifications.rst ├── renovate.json ├── setup.py ├── tests ├── __init__.py ├── scripts │ ├── __init__.py │ └── test_client.py ├── targets │ ├── test_android.py │ ├── test_ios.py │ └── utils.py ├── test_buildops.py ├── test_buildozer.py ├── test_logger.py └── test_specparser.py └── tox.ini /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = ["tests/**"] 4 | 5 | [[analyzers]] 6 | name = "python" 7 | enabled = true 8 | 9 | [analyzers.meta] 10 | runtime_version = "3.x.x" 11 | 12 | 13 | [[analyzers]] 14 | name = "docker" 15 | enabled = true 16 | 17 | [analyzers.meta] 18 | dockerfile_paths = [ 19 | "dockerfile_dev", 20 | "dockerfile_prod" 21 | ] 22 | 23 | [[analyzers]] 24 | name = "ruby" 25 | enabled = true 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | ### Versions 14 | 15 | * Python: 16 | * OS: 17 | * Buildozer: 18 | 19 | ### Description 20 | 21 | // REPLACE ME: What are you trying to get done, what has happened, what went wrong, and what did you expect? 22 | 23 | ### buildozer.spec 24 | Command: 25 | ```sh 26 | // REPLACE ME: buildozer command ran? e.g. buildozer android debug 27 | ``` 28 | 29 | Spec file: 30 | ``` 31 | // REPLACE ME: Paste your buildozer.spec file here 32 | ``` 33 | 34 | ### Logs 35 | 36 | ``` 37 | // REPLACE ME: Paste the build ouput containing the error 38 | ``` 39 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | env: 4 | PYTHONFORANDROID_PREREQUISITES_INSTALL_INTERACTIVE: 0 5 | 6 | name: Android Integration 7 | jobs: 8 | Integration: 9 | strategy: 10 | matrix: 11 | os: 12 | - 'ubuntu-latest' 13 | - 'macOs-latest' 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: Setup python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: '3.x' 20 | - name: Setup Java 21 | uses: actions/setup-java@v4 22 | with: 23 | java-version: '17' 24 | distribution: 'temurin' 25 | - uses: actions/checkout@v4 26 | - name: Setup environment 27 | run: | 28 | pip install . 29 | - run: buildozer --help 30 | - run: buildozer init 31 | - name: SDK, NDK and p4a download 32 | run: | 33 | sed -i.bak "s/# android.accept_sdk_license = False/android.accept_sdk_license = True/" buildozer.spec 34 | sed -i.bak "s/#p4a.branch = master/p4a.branch = develop/" buildozer.spec 35 | buildozer android p4a -- --help 36 | # Install OS specific dependencies 37 | - name: Install Linux dependencies 38 | if: matrix.os == 'ubuntu-latest' 39 | # Required by some p4a recipes, but not 40 | # installed by p4a on Linux. 41 | run: sudo apt -y install automake 42 | - name: Debug Build 43 | run: | 44 | touch main.py 45 | buildozer android debug 46 | - name: Release Build (aab) 47 | run: | 48 | touch main.py 49 | export BUILDOZER_ALLOW_ORG_TEST_DOMAIN=1 50 | buildozer android release 51 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | env: 9 | DOCKERHUB_IMAGE: kivy/buildozer 10 | GHCR_IMAGE: ghcr.io/${{ github.repository }} 11 | SHOULD_PUBLISH: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')) }} 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-24.04 16 | timeout-minutes: 60 17 | permissions: 18 | contents: read 19 | packages: write 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: docker/setup-buildx-action@v3 23 | 24 | # Login to DockerHub 25 | - uses: docker/login-action@v3 26 | if: ${{ env.SHOULD_PUBLISH == 'true' }} 27 | with: 28 | username: ${{ secrets.DOCKERHUB_USERNAME }} 29 | password: ${{ secrets.DOCKERHUB_TOKEN }} 30 | 31 | # Login to GHCR 32 | - uses: docker/login-action@v3 33 | if: ${{ env.SHOULD_PUBLISH == 'true' }} 34 | with: 35 | registry: ghcr.io 36 | username: ${{ github.actor }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Build and Push Multi-platform Image 40 | uses: docker/build-push-action@v6 41 | with: 42 | push: ${{ env.SHOULD_PUBLISH == 'true' }} 43 | tags: | 44 | ${{ env.DOCKERHUB_IMAGE }}:latest 45 | ${{ env.GHCR_IMAGE }}:latest 46 | platforms: linux/amd64,linux/arm64 47 | cache-from: type=registry,ref=${{ env.DOCKERHUB_IMAGE }}:latest 48 | cache-to: ${{ env.SHOULD_PUBLISH == 'true' && format('type=registry,ref={0}:latest,mode=max', env.DOCKERHUB_IMAGE) || '' }} 49 | 50 | - name: Local Build for Testing 51 | uses: docker/build-push-action@v6 52 | with: 53 | # Load image into local Docker daemon 54 | load: true 55 | cache-from: type=registry,ref=${{ env.DOCKERHUB_IMAGE }}:latest 56 | tags: ${{ env.DOCKERHUB_IMAGE }}:latest 57 | # Run the locally built image to test it 58 | - name: Docker run 59 | run: docker run ${{ env.DOCKERHUB_IMAGE }} --version 60 | 61 | update-readme: 62 | runs-on: ubuntu-24.04 63 | needs: build 64 | steps: 65 | - uses: actions/checkout@v4 66 | - uses: peter-evans/dockerhub-description@v4 67 | if: ${{ env.SHOULD_PUBLISH == 'true' }} 68 | with: 69 | username: ${{ secrets.DOCKERHUB_USERNAME }} 70 | password: ${{ secrets.DOCKERHUB_TOKEN }} 71 | repository: ${{ env.DOCKERHUB_IMAGE }} 72 | readme-filepath: README.md 73 | -------------------------------------------------------------------------------- /.github/workflows/ios.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: iOS 3 | jobs: 4 | Integration: 5 | name: "Integration (${{ matrix.runs_on }}, ${{ matrix.python }})" 6 | runs-on: ${{ matrix.runs_on }} 7 | strategy: 8 | matrix: 9 | # macos-latest (ATM macos-14) runs on Apple Silicon, 10 | # macos-13 runs on Intel 11 | runs_on: [macos-latest, macos-13] 12 | steps: 13 | - name: Setup python 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: '3.x' 17 | - uses: actions/checkout@v4 18 | - name: Setup environment 19 | run: | 20 | pip install .[ios] 21 | - name: Check buildozer installation 22 | run: | 23 | buildozer --help 24 | - name: Initialize buildozer in project folder 25 | run: | 26 | buildozer init 27 | - name: Install dependencies 28 | run: | 29 | brew install autoconf automake libtool pkg-config 30 | - name: buildozer ios debug 31 | run: | 32 | touch main.py 33 | buildozer ios debug 34 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | name: No Response 2 | 3 | # Both `issue_comment` and `scheduled` event types are required for this Action 4 | # to work properly. 5 | on: 6 | issue_comment: 7 | types: [created] 8 | schedule: 9 | # Schedule for an arbitrary time (5am) once every day 10 | - cron: '* 5 * * *' 11 | 12 | jobs: 13 | noResponse: 14 | # Don't run if in a fork 15 | if: github.repository_owner == 'kivy' 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: lee-dohm/no-response@9bb0a4b5e6a45046f00353d5de7d90fb8bd773bb 19 | # This commit hash targets release v0.5.0 of lee-dohm/no-response. 20 | # Targeting a commit hash instead of a tag has been done for security reasons. 21 | # Please be aware that the commit hash specifically targets the "Automatic compilation" 22 | # done by `github-actions[bot]` as the `no-response` Action needs to be compiled. 23 | with: 24 | token: ${{ github.token }} 25 | daysUntilClose: 42 26 | responseRequiredLabel: 'awaiting-reply' 27 | closeComment: > 28 | This issue has been automatically closed because there has been no response 29 | to our request for more information from the original author. With only the 30 | information that is currently in the issue, we don't have the means 31 | to take action. Please reach out if you have or find the answers we need so 32 | that we can investigate further. 33 | -------------------------------------------------------------------------------- /.github/workflows/pypi-release.yml: -------------------------------------------------------------------------------- 1 | name: PyPI release 2 | on: [push] 3 | 4 | jobs: 5 | pypi_release: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - name: Set up Python 3.x 10 | uses: actions/setup-python@v5 11 | with: 12 | python-version: 3.x 13 | - name: Install build dependencies 14 | run: | 15 | python -m pip install --upgrade setuptools wheel twine build 16 | - name: Build 17 | run: | 18 | python -m build 19 | twine check dist/* 20 | - name: Publish package 21 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 22 | uses: pypa/gh-action-pypi-publish@v1.12.4 23 | with: 24 | user: __token__ 25 | password: ${{ secrets.pypi_password }} 26 | -------------------------------------------------------------------------------- /.github/workflows/support.yml: -------------------------------------------------------------------------------- 1 | # When a user creates an issue that is actually a support request, it should 2 | # be closed with a friendly comment. 3 | # 4 | # This triggers on an issue being labelled with the `support` tag. 5 | 6 | name: 'Support Requests' 7 | 8 | on: 9 | issues: 10 | types: [labeled, unlabeled, reopened] 11 | 12 | permissions: 13 | issues: write 14 | 15 | jobs: 16 | action: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: dessant/support-requests@v4 20 | with: 21 | github-token: ${{ github.token }} 22 | support-label: 'support' 23 | issue-comment: > 24 | 👋 @{issue-author}, 25 | 26 | Sorry to hear you are having difficulties with Kivy's Buildozer; Kivy unites a number of different technologies, so building apps can be temperamental. 27 | 28 | We try to use GitHub issues only to track work for developers to do to fix bugs and add new features to Buildozer. This issue has been closed, because it doesn't describe a bug or new feature request for Buildozer. 29 | 30 | There is a mailing list and a Discord channel to support Kivy users debugging their own systems, which should be able to help. They are linked in the [ReadMe](https://github.com/kivy/buildozer#support). 31 | 32 | Of course, if it turns out you have stumbled over a bug in Buildozer, we do want to hear about it here. The support channels should be able to help you craft an appropriate bug report. 33 | 34 | close-issue: true 35 | lock-issue: false 36 | -------------------------------------------------------------------------------- /.github/workflows/test_python.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Tests 3 | jobs: 4 | Tests: 5 | name: base 6 | strategy: 7 | matrix: 8 | python: 9 | - '3.8' 10 | - '3.9' 11 | - '3.10' 12 | - '3.11' 13 | os: 14 | - 'ubuntu-latest' 15 | - 'macOs-latest' 16 | architecture: 17 | - 'x64' 18 | 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Setup python 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ matrix.python }} 27 | 28 | - name: Requirements 29 | run: | 30 | pip install -U coveralls setuptools tox>=2.0 31 | - name: Tox 32 | run: tox 33 | - name: Coveralls 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | COVERALLS_SERVICE_NAME: github-actions 37 | COVERALLS_FLAG_NAME: python-${{ matrix.python }}-${{ matrix.os }} 38 | COVERALLS_PARALLEL: true 39 | run: coveralls 40 | 41 | Coveralls: 42 | needs: Tests 43 | runs-on: ubuntu-latest 44 | container: python:3-slim 45 | steps: 46 | - name: Finished 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | run: | 50 | pip install coveralls 51 | coveralls --finish 52 | 53 | Docker: 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v4 57 | - name: Docker build 58 | run: docker build --tag=kivy/buildozer . 59 | - name: Docker run 60 | run: docker run kivy/buildozer --version 61 | 62 | Documentation: 63 | runs-on: ubuntu-latest 64 | steps: 65 | - uses: actions/checkout@v4 66 | - name: Requirements 67 | run: pip install -U sphinx 68 | - name: Check links 69 | run: sphinx-build -b linkcheck docs/source docs/build 70 | - name: Generate documentation 71 | run: sphinx-build docs/source docs/build 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | .*.swp 5 | .*.swo 6 | *.egg 7 | *.egg-info 8 | dist 9 | build 10 | eggs 11 | parts 12 | bin 13 | var 14 | sdist 15 | develop-eggs 16 | .installed.cfg 17 | .idea 18 | 19 | # Installer logs 20 | pip-log.txt 21 | 22 | # Unit test / coverage reports 23 | .coverage 24 | .tox 25 | 26 | #Translations 27 | *.mo 28 | 29 | #Mr Developer 30 | .mr.developer.cfg 31 | MANIFEST 32 | 33 | release\.log\.utf-8\.tmp 34 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | In the interest of fostering an open and welcoming community, we as 2 | contributors and maintainers need to ensure participation in our project and 3 | our sister projects is a harassment-free and positive experience for everyone. 4 | It is vital that all interaction is conducted in a manner conveying respect, 5 | open-mindedness and gratitude. 6 | 7 | Please consult the [latest Kivy Code of Conduct](https://github.com/kivy/kivy/blob/master/CODE_OF_CONDUCT.md). 8 | 9 | -------------------------------------------------------------------------------- /CONTACT.md: -------------------------------------------------------------------------------- 1 | # Contacting the Kivy Team 2 | 3 | If you are looking to contact the Kivy Team (who are responsible for managing 4 | the Buildozer project), including looking for support, please see our 5 | latest [Contact Us](https://github.com/kivy/kivy/blob/master/CONTACT.md) 6 | document. 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Buildozer is part of the [Kivy](https://kivy.org) ecosystem - a large group of 4 | products used by many thousands of developers for free, but it 5 | is built entirely by the contributions of volunteers. We welcome (and rely on) 6 | users who want to give back to the community by contributing to the project. 7 | 8 | Contributions can come in many forms. See the latest 9 | [Contribution Guidelines](https://github.com/kivy/kivy/blob/master/CONTRIBUTING.md) 10 | for general guidelines of how you can help us. 11 | 12 | --- 13 | 14 | If you would like to work on Buildozer, you can set up a development build: 15 | ```bash 16 | git clone https://github.com/kivy/buildozer 17 | cd buildozer 18 | python setup.py build 19 | pip install -e . 20 | ``` 21 | --- 22 | 23 | Buildozer uses python-for-android, that is architected to be extensible with 24 | new recipes and new bootstraps. 25 | 26 | If you do develop a new recipe on python-for-android, here is how to test it: 27 | 28 | #. Fork `Python for Android `_, and 29 | clone your own version (this will allow easy contribution later):: 30 | 31 | ```bash 32 | git clone https://github.com/YOURNAME/python-for-android 33 | ``` 34 | 35 | #. Change your `buildozer.spec` to reference your version:: 36 | 37 | p4a.source_dir = /path/to/your/python-for-android 38 | 39 | #. Copy your recipe into `python-for-android/recipes/YOURLIB/recipe.sh` 40 | 41 | #. Rebuild. 42 | 43 | When your recipe works, you can ask us to 44 | include it in the python-for-android project, by issuing a Pull Request: 45 | 46 | #. Create a branch:: 47 | 48 | ```bash 49 | git checkout --track -b recipe-YOURLIB origin/master 50 | ``` 51 | 52 | #. Add and commit:: 53 | 54 | ```bash 55 | git add python-for-android/recipes/YOURLIB/* 56 | git commit -am 'Add support for YOURLIB` 57 | ``` 58 | 59 | #. Push to GitHub 60 | 61 | ```bash 62 | git push origin master 63 | ``` 64 | 65 | #. Go to `https://github.com/YOURNAME/python-for-android`, and you should see 66 | your new branch and a button "Pull Request" on it. Use it, write a 67 | description about what you did, and Send! -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for providing buildozer 2 | # 3 | # Build with: 4 | # docker build --tag=kivy/buildozer . 5 | # 6 | # Or for macOS using Docker Desktop: 7 | # 8 | # docker buildx build --platform=linux/amd64 -t kivy/buildozer . 9 | # 10 | # In order to give the container access to your current working directory 11 | # it must be mounted using the --volume option. 12 | # Run with (e.g. `buildozer --version`): 13 | # docker run \ 14 | # --volume "$HOME/.buildozer":/home/user/.buildozer \ 15 | # --volume "$PWD":/home/user/hostcwd \ 16 | # kivy/buildozer --version 17 | # 18 | # Or for interactive shell: 19 | # docker run --interactive --tty --rm \ 20 | # --volume "$HOME/.buildozer":/home/user/.buildozer \ 21 | # --volume "$PWD":/home/user/hostcwd \ 22 | # --entrypoint /bin/bash \ 23 | # kivy/buildozer 24 | # 25 | # If you get a `PermissionError` on `/home/user/.buildozer/cache`, 26 | # try updating the permissions from the host with: 27 | # sudo chown $USER -R ~/.buildozer 28 | # Or simply recreate the directory from the host with: 29 | # rm -rf ~/.buildozer && mkdir ~/.buildozer 30 | 31 | FROM ubuntu:22.04 32 | 33 | ENV USER="user" 34 | ENV HOME_DIR="/home/${USER}" 35 | ENV WORK_DIR="${HOME_DIR}/hostcwd" \ 36 | SRC_DIR="${HOME_DIR}/src" \ 37 | PATH="${HOME_DIR}/.local/bin:${PATH}" 38 | 39 | # configures locale 40 | RUN apt update -qq > /dev/null \ 41 | && DEBIAN_FRONTEND=noninteractive apt install -qq --yes --no-install-recommends \ 42 | locales && \ 43 | locale-gen en_US.UTF-8 44 | ENV LANG="en_US.UTF-8" \ 45 | LANGUAGE="en_US.UTF-8" \ 46 | LC_ALL="en_US.UTF-8" 47 | 48 | # system requirements to build most of the recipes 49 | RUN apt update -qq > /dev/null \ 50 | && DEBIAN_FRONTEND=noninteractive apt install -qq --yes --no-install-recommends \ 51 | autoconf \ 52 | automake \ 53 | build-essential \ 54 | ccache \ 55 | cmake \ 56 | gettext \ 57 | git \ 58 | libffi-dev \ 59 | libltdl-dev \ 60 | libssl-dev \ 61 | libtool \ 62 | openjdk-17-jdk \ 63 | patch \ 64 | pkg-config \ 65 | python3-pip \ 66 | python3-setuptools \ 67 | sudo \ 68 | unzip \ 69 | zip \ 70 | zlib1g-dev 71 | 72 | # prepares non root env 73 | RUN useradd --create-home --shell /bin/bash ${USER} 74 | # with sudo access and no password 75 | RUN usermod -append --groups sudo ${USER} 76 | RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers 77 | 78 | USER ${USER} 79 | WORKDIR ${WORK_DIR} 80 | COPY --chown=user:user . ${SRC_DIR} 81 | 82 | # installs buildozer and dependencies 83 | RUN pip3 install --user --upgrade "Cython<3.0" wheel pip ${SRC_DIR} 84 | 85 | ENTRYPOINT ["buildozer"] 86 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ for Buildozer 2 | 3 | ## Introduction 4 | 5 | Buildozer is a development tool for turning [Python](https://www.python.org/) 6 | applications into binary packages ready for installation on any of a number of 7 | platforms, including mobile devices. 8 | 9 | The app developer provides a single "buildozer.spec" file, which describes the 10 | application's requirements and settings, such as title and icons. Buildozer can 11 | then create installable packages for Android, iOS, Windows, macOS and/or Linux. 12 | 13 | Buildozer is managed by the [Kivy Team](https://kivy.org/about.html). It relies 14 | on its sibling projects: 15 | [python-for-android](https://github.com/kivy/python-for-android/) and 16 | [Kivy for iOS](https://github.com/kivy/kivy-ios/). It has features to make 17 | building apps using the [Kivy framework](https://github.com/kivy/kivy) easier, 18 | but it can be used independently - even with other GUI frameworks. 19 | 20 | ## How do I write my own recipes? 21 | 22 | Instructions on how to write your own recipes is available in the 23 | [Kivy for iOS](https://github.com/kivy/kivy-ios/) and 24 | [python-for-android documentation](https://python-for-android.readthedocs.io/en/latest/recipes.html). 25 | 26 | Instructions on how to test your own recipes from Buildozer is available in the 27 | [Buildozer Contribution Guidelines](CONTRIBUTING.md). 28 | 29 | > [!NOTE] 30 | > This document is very short at the moment. Please contribute some FAQ 31 | > questions and answers. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2017 Kivy Team and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *COPYING 2 | include *CHANGELOG.md 3 | include *README.md 4 | recursive-include buildozer *.spec 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Buildozer 2 | 3 | Buildozer is a development tool for turning [Python](https://www.python.org/) 4 | applications into binary packages ready for installation on any of a number of 5 | platforms, including mobile devices. 6 | 7 | The app developer provides a single "buildozer.spec" file, which describes the 8 | application's requirements and settings, such as title and icons. Buildozer can 9 | then create installable packages for Android, iOS, Windows, macOS and/or Linux. 10 | 11 | Buildozer is managed by the [Kivy Team](https://kivy.org/about.html). It relies 12 | on its sibling projects: 13 | [python-for-android](https://github.com/kivy/python-for-android/) and 14 | [Kivy for iOS](https://github.com/kivy/kivy-ios/). It has features to make 15 | building apps using the [Kivy framework](https://github.com/kivy/kivy) easier, 16 | but it can be used independently - even with other GUI frameworks. 17 | 18 | For Android, buildozer will automatically download and prepare the 19 | build dependencies. For more information, see 20 | [Android SDK NDK Information](https://github.com/kivy/kivy/wiki/Android-SDK-NDK-Information). 21 | 22 | > [!NOTE] 23 | > This tool is unrelated to the online build service, 24 | > `buildozer.io`. 25 | 26 | [![Backers on Open Collective](https://opencollective.com/kivy/backers/badge.svg)](#backers) 27 | [![Sponsors on Open Collective](https://opencollective.com/kivy/sponsors/badge.svg)](#sponsors) 28 | [![GitHub contributors](https://img.shields.io/github/contributors-anon/kivy/buildozer)](https://github.com/kivy/buildozer/graphs/contributors) 29 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) 30 | 31 | ![PyPI - Version](https://img.shields.io/pypi/v/buildozer) 32 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/buildozer) 33 | 34 | [![Tests](https://github.com/kivy/buildozer/workflows/Tests/badge.svg)](https://github.com/kivy/buildozer/actions?query=workflow%3ATests) 35 | [![Android](https://github.com/kivy/buildozer/workflows/Android/badge.svg)](https://github.com/kivy/buildozer/actions?query=workflow%3AAndroid) 36 | [![iOS](https://github.com/kivy/buildozer/workflows/iOS/badge.svg)](https://github.com/kivy/buildozer/actions?query=workflow%3AiOS) 37 | [![Coverage Status](https://coveralls.io/repos/github/kivy/buildozer/badge.svg)](https://coveralls.io/github/kivy/buildozer) 38 | [![Docker](https://github.com/kivy/buildozer/actions/workflows/docker.yml/badge.svg)](https://github.com/kivy/buildozer/actions/workflows/docker.yml) 39 | 40 | 41 | ## Installation 42 | 43 | Buildozer 44 | 45 | ## Installing Buildozer with target Python 3 (default): 46 | 47 | Buildozer incorporates a number of technologies, and has a complicated 48 | dependencies, including platform dependencies outside of Python. 49 | 50 | This means installation is more than a simple `pip install`; many of our support 51 | requests are related to missing dependencies. 52 | 53 | So, it is important to follow the instructions carefully. 54 | 55 | Please see the 56 | [Installation documentation](https://buildozer.readthedocs.io/en/latest/installation.html) 57 | specific to this version. 58 | 59 | ## Buildozer Docker image 60 | 61 | A Dockerfile is available to use buildozer through a Docker environment. 62 | 63 | - Build with: 64 | 65 | ```bash 66 | docker build --tag=kivy/buildozer . 67 | ``` 68 | 69 | For macOS, build with: 70 | 71 | ```bash 72 | docker buildx build --platform=linux/amd64 --tag=kivy/buildozer . 73 | ``` 74 | 75 | - Run with: 76 | 77 | ```bash 78 | docker run --volume "$(pwd)":/home/user/hostcwd kivy/buildozer --version 79 | ``` 80 | 81 | ### Example Build with Caching 82 | - Build and keep downloaded SDK and NDK in `~/.buildozer` directory: 83 | 84 | ```bash 85 | docker run -v $HOME/.buildozer:/home/user/.buildozer -v $(pwd):/home/user/hostcwd kivy/buildozer android debug 86 | ``` 87 | 88 | The image is published to both Docker Hub and GitHub Container Registry and can be pulled from both: 89 | 90 | ```bash 91 | docker pull kivy/buildozer:latest 92 | docker pull ghcr.io/kivy/buildozer:latest 93 | ``` 94 | 95 | ## Buildozer GitHub action 96 | 97 | Use [ArtemSBulgakov/buildozer-action@v1](https://github.com/ArtemSBulgakov/buildozer-action) 98 | to build your packages automatically on push or pull request. 99 | See [full workflow example](https://github.com/ArtemSBulgakov/buildozer-action#full-workflow). 100 | 101 | ## Usage 102 | 103 | ```yml 104 | Usage: 105 | buildozer [--profile ] [--verbose] [target] ... 106 | buildozer --version 107 | 108 | Available targets: 109 | android Android target, based on python-for-android project 110 | ios iOS target, based on kivy-ios project 111 | 112 | Global commands (without target): 113 | distclean Clean the whole Buildozer environment 114 | help Show the Buildozer help 115 | init Create an initial buildozer.spec in the current directory 116 | serve Serve the bin directory via SimpleHTTPServer 117 | setdefault Set the default command to run when no arguments are given 118 | version Show the Buildozer version 119 | 120 | Target commands: 121 | clean Clean the target environment 122 | update Update the target dependencies 123 | debug Build the application in debug mode 124 | release Build the application in release mode 125 | deploy Deploy the application on the device 126 | run Run the application on the device 127 | serve Serve the bin directory via SimpleHTTPServer 128 | 129 | Target "ios" commands: 130 | list_identities List the available identities to use for signing. 131 | xcode Open the xcode project. 132 | 133 | Target "android" commands: 134 | adb Run adb from the Android SDK. Args must come after --, or 135 | use --alias to make an alias 136 | logcat Show the log from the device 137 | p4a Run p4a commands. Args must come after --, or use --alias 138 | to make an alias 139 | ``` 140 | 141 | ## Examples of Buildozer commands 142 | 143 | ```bash 144 | # buildozer target command 145 | buildozer android clean 146 | buildozer android update 147 | buildozer android deploy 148 | buildozer android debug 149 | buildozer android release 150 | 151 | # or all in one (compile in debug, deploy on device) 152 | buildozer android debug deploy 153 | 154 | # set the default command if nothing set 155 | buildozer setdefault android debug deploy run 156 | ``` 157 | 158 | ## `buildozer.spec` 159 | 160 | Run `buildozer init` to have a new `buildozer.spec` file copied into the current 161 | working directory. Edit it before running your first build. 162 | 163 | See [buildozer/default.spec](https://raw.github.com/kivy/buildozer/master/buildozer/default.spec) for the template. 164 | 165 | ## Default config 166 | 167 | You can override the value of any `buildozer.spec` config token by 168 | setting an appropriate environment variable. These are all of the 169 | form `$SECTION_TOKEN`, where SECTION is the config file section and 170 | TOKEN is the config token to override. Dots are replaced by 171 | underscores. 172 | 173 | For example, here are some config tokens from the [app] section of the 174 | config, along with the environment variables that would override them. 175 | 176 | - `title` -> `$APP_TITLE` 177 | - `package.name` -> `$APP_PACKAGE_NAME` 178 | - `p4a.source_dir` -> `$APP_P4A_SOURCE_DIR` 179 | 180 | ## License 181 | 182 | Buildozer is [MIT licensed](LICENSE), actively developed by a great 183 | community and is supported by many projects managed by the 184 | [Kivy Organization](https://www.kivy.org/about.html). 185 | 186 | ## Documentation 187 | 188 | [Documentation for this repository](https://buildozer.readthedocs.io/). 189 | 190 | ## Support 191 | 192 | Are you having trouble using Buildozer or any of its related projects in the Kivy 193 | ecosystem? 194 | Is there an error you don’t understand? Are you trying to figure out how to use 195 | it? We have volunteers who can help! 196 | 197 | The best channels to contact us for support are listed in the latest 198 | [Contact Us](https://github.com/kivy/buildozer/blob/master/CONTACT.md) document. 199 | 200 | ## Contributing 201 | 202 | Buildozer is part of the [Kivy](https://kivy.org) ecosystem - a large group of 203 | products used by many thousands of developers for free, but it 204 | is built entirely by the contributions of volunteers. We welcome (and rely on) 205 | users who want to give back to the community by contributing to the project. 206 | 207 | Contributions can come in many forms. See the latest 208 | [Contribution Guidelines](https://github.com/kivy/buildozer/blob/master/CONTRIBUTING.md) 209 | for how you can help us. 210 | 211 | ## Code of Conduct 212 | 213 | In the interest of fostering an open and welcoming community, we as 214 | contributors and maintainers need to ensure participation in our project and 215 | our sister projects is a harassment-free and positive experience for everyone. 216 | It is vital that all interaction is conducted in a manner conveying respect, 217 | open-mindedness and gratitude. 218 | 219 | Please consult the [latest Code of Conduct](https://github.com/kivy/buildozer/blob/master/CODE_OF_CONDUCT.md). 220 | 221 | ## Contributors 222 | 223 | This project exists thanks to 224 | [all the people who contribute](https://github.com/kivy/buildozer/graphs/contributors). 225 | [[Become a contributor](CONTRIBUTING.md)]. 226 | 227 | 228 | 229 | ## Backers 230 | 231 | Thank you to [all of our backers](https://opencollective.com/kivy)! 232 | 🙏 [[Become a backer](https://opencollective.com/kivy#backer)] 233 | 234 | 235 | 236 | ## Sponsors 237 | 238 | Special thanks to 239 | [all of our sponsors, past and present](https://opencollective.com/kivy). 240 | Support this project by 241 | [[becoming a sponsor](https://opencollective.com/kivy#sponsor)]. 242 | 243 | Here are our top current sponsors. Please click through to see their websites, 244 | and support them as they support us. 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | -------------------------------------------------------------------------------- /buildozer/__main__.py: -------------------------------------------------------------------------------- 1 | from buildozer.scripts.client import main 2 | 3 | if __name__ == '__main__': 4 | main() 5 | -------------------------------------------------------------------------------- /buildozer/buildops.py: -------------------------------------------------------------------------------- 1 | """ 2 | A set of basic cross-platform OS-level operations that are required to build. 3 | 4 | These operations don't require any knowledge of the target being built. 5 | 6 | Changes to the system are logged. 7 | """ 8 | 9 | import codecs 10 | from collections import namedtuple 11 | from glob import glob 12 | import os 13 | from os.path import join, exists, realpath, expanduser 14 | from pathlib import Path 15 | import pexpect 16 | from queue import Queue, Empty 17 | from sys import exit, stdout, stderr, platform 18 | from subprocess import Popen, PIPE 19 | from shutil import copyfile, rmtree, copytree, move, which 20 | import shlex 21 | import time 22 | import tarfile 23 | from threading import Thread 24 | from urllib.request import Request, urlopen 25 | from zipfile import ZipFile 26 | 27 | from buildozer.exceptions import BuildozerCommandException 28 | from buildozer.logger import Logger 29 | 30 | LOGGER = Logger() 31 | 32 | 33 | def checkbin(friendly_name, fn): 34 | """Find a command on the system path.""" 35 | LOGGER.debug("Search for {0}".format(friendly_name)) 36 | executable_location = which(str(fn)) 37 | if executable_location: 38 | LOGGER.debug(" -> found at {0}".format(executable_location)) 39 | return realpath(executable_location) 40 | LOGGER.error("{} not found, please install it.".format(friendly_name)) 41 | exit(1) 42 | 43 | 44 | def mkdir(dn): 45 | if exists(dn): 46 | return 47 | LOGGER.debug("Create directory {0}".format(dn)) 48 | os.makedirs(dn) 49 | 50 | 51 | def rmdir(dn): 52 | if not exists(dn): 53 | return 54 | LOGGER.debug("Remove directory and subdirectory {}".format(dn)) 55 | rmtree(dn) 56 | 57 | 58 | def file_matches(patterns): 59 | result = [] 60 | for pattern in patterns: 61 | matches = glob(expanduser(pattern.strip())) 62 | result.extend(matches) 63 | return result 64 | 65 | 66 | def file_exists(path): 67 | """ 68 | return if file exists. 69 | Accept a Path instance or path string 70 | """ 71 | return Path(path).exists() 72 | 73 | 74 | def file_remove(path): 75 | """ 76 | Remove target file if present. 77 | Accept a Path instance or path string. 78 | """ 79 | path = Path(path) 80 | if path.exists(): 81 | LOGGER.debug("Removing {0}".format(path)) 82 | path.unlink() 83 | 84 | 85 | def rename(source, target, cwd="."): 86 | """Rename a file or directory from source to target. 87 | 88 | If target is an existing directory, move into that directory. 89 | 90 | If target is an existing file, the behaviour is OS-dependent.""" 91 | 92 | source = Path(cwd, source) 93 | target = Path(cwd, target) 94 | LOGGER.debug("Rename {0} to {1}".format(source, target)) 95 | move(source, target) 96 | 97 | 98 | def file_copy(source, target, cwd="."): 99 | """Copy a single file from source to target. 100 | 101 | If target is an existing directory, copy into that directory. 102 | 103 | If target is an existing file, overwrite.""" 104 | 105 | source = Path(cwd, source) 106 | target = Path(cwd, target) 107 | LOGGER.debug("Copy {0} to {1}".format(source, target)) 108 | copyfile(source, target) 109 | 110 | 111 | def file_extract(archive, env, cwd="."): 112 | """ 113 | Extract compressed files. 114 | Also, run .bin files, in the context of env. 115 | 116 | Accepts path or path strings. 117 | """ 118 | path = Path(cwd, archive) 119 | 120 | if any( 121 | str(archive).endswith(extension) 122 | for extension in (".tgz", ".tar.gz", ".tbz2", ".tar.bz2") 123 | ): 124 | LOGGER.debug("Extracting {0} to {1}".format(archive, cwd)) 125 | with tarfile.open(path, "r") as compressed_file: 126 | compressed_file.extractall(cwd) 127 | return 128 | 129 | if path.suffix == ".zip": 130 | LOGGER.debug("Extracting {0} to {1}".format(archive, cwd)) 131 | if platform == "win32": 132 | # This won't work on Unix/OSX, because Android NDK (for example) 133 | # relies on non-standard handling of file permissions and symbolic 134 | # links that Python's zipfile doesn't support. 135 | # Windows doesn't support them either. 136 | with ZipFile(path, "r") as compressed_file: 137 | compressed_file.extractall(cwd) 138 | return 139 | else: 140 | # This won't work on Windows, because there is no unzip command 141 | # there 142 | return_code = cmd( 143 | ["unzip", "-q", join(cwd, archive)], cwd=cwd, env=env 144 | ).return_code 145 | if return_code != 0: 146 | raise BuildozerCommandException( 147 | "Unzip gave bad return code: {}".format(return_code)) 148 | return 149 | 150 | if path.suffix == ".bin": 151 | # To process the bin files for linux and darwin systems 152 | assert platform in ("darwin", "linux") 153 | LOGGER.debug("Executing {0}".format(archive)) 154 | 155 | cmd(["chmod", "a+x", str(archive)], cwd=cwd, env=env) 156 | cmd([f"./{archive}"], cwd=cwd, env=env) 157 | return 158 | 159 | raise ValueError("Unhandled extraction for type {0}".format(archive)) 160 | 161 | 162 | def file_copytree(source, target): 163 | """ 164 | Move an entire directory tree from source to target. 165 | 166 | If source is a single file, it will copy just the one file, but target 167 | must be a filename, not directory. 168 | """ 169 | source = Path(source) 170 | target = Path(target) 171 | 172 | LOGGER.debug("copy {} to {}".format(source, target)) 173 | if source.is_dir(): 174 | copytree(source, target) 175 | else: 176 | copyfile(source, target) 177 | 178 | 179 | class _StreamReader: 180 | """ 181 | Allow streams to be read in real-time, with a timeout. 182 | 183 | Works cross-platform, unlike select. 184 | """ 185 | 186 | def __init__(self, stdout_, stderr_): 187 | self._queue = Queue() 188 | self._completed_count = 0 # How many streams have been finished. 189 | for stream, id in [(stdout_, "out"), (stderr_, "err")]: 190 | t = Thread(target=self._fill_queue, args=(stream, id), daemon=True) 191 | t.start() 192 | 193 | def _fill_queue(self, stream, id): 194 | if hasattr(stream, "read1"): 195 | # Read data straight from buffer so partial lines are sent 196 | # immediately. 197 | while not stream.closed: 198 | data = stream.read1() 199 | if data: 200 | self._queue.put((data, id)) 201 | elif not stream.closed: 202 | # Avoid busy looping 203 | time.sleep(0.1) 204 | else: 205 | # Use line-buffering. Partial lines will not be sent until 206 | # completed. 207 | for line in stream: 208 | self._queue.put((line, id)) 209 | self._queue.put("completed") 210 | 211 | def read(self, timeout=None): 212 | """ 213 | returns a tuple (stdin_output, stderr_output) 214 | where one will be None. 215 | or None if timed out or completed. 216 | 217 | Will block unbounded if timeout is None 218 | """ 219 | 220 | if self._completed_count >= 2: 221 | return None # Already completed. 222 | 223 | try: 224 | while True: # Repeat if you get a completed. 225 | item = self._queue.get(block=True, timeout=timeout) 226 | if item == "completed": 227 | self._completed_count += 1 228 | if self._completed_count == 2: 229 | return None 230 | # One stream is complete. 231 | # Keep looping until both streams are complete. 232 | # Assume if one completes, the other won't block before it 233 | # completes, so there is no concern with exceeding the 234 | # cumulative timeout when looping. 235 | else: 236 | line, id = item 237 | if id == "out": 238 | return line, None 239 | else: 240 | return None, line 241 | except Empty: 242 | # Timeout 243 | return None 244 | 245 | 246 | CommandResult = namedtuple("CommandResult", "stdout stderr return_code") 247 | 248 | 249 | def cmd( 250 | command, 251 | env, 252 | cwd=None, 253 | get_stdout=False, 254 | get_stderr=False, 255 | break_on_error=True, 256 | run_condition=None, 257 | show_output=None, 258 | quiet=False, 259 | ) -> CommandResult: 260 | """run a command as a subprocess, with the ability to display progress 261 | and to abort the process early. 262 | 263 | returns CommandResult which includes stdout text, stderr text, 264 | and process return code. 265 | 266 | command parameter is a tuple (or iterable collection) of the command and 267 | then its parameters 268 | 269 | if a run_condition callback is provided, it is polled once per second 270 | and the subprocess will be terminated if it returns false. 271 | 272 | If show_output is true, stdout and stderr will be echoed. 273 | 274 | If get_stdout or get_stderr are false, they will not be returned. 275 | 276 | If break_on_error is set, an exception will be raised if an error code is 277 | returned, and details with be logged. Note: On some platforms, a 278 | termination due to run_condition returning False will result in an 279 | error code. 280 | 281 | quiet parameter reduces logging; useful to keep passwords in command lines 282 | out of the log. 283 | 284 | The env parameter is deliberately not optional, to ensure it is considered 285 | during the migration to use this library. Once completed, it can return 286 | to having a default of None. 287 | 288 | """ 289 | 290 | show_output = LOGGER.log_level > 1 if show_output is None else show_output 291 | env = os.environ if env is None else env 292 | 293 | # Just in case a path-like is passed as a command or param. 294 | command = tuple(str(item) for item in command) 295 | 296 | if not quiet: 297 | LOGGER.debug("Run {0!r} ...".format(" ".join(command))) 298 | LOGGER.debug("Cwd {}".format(cwd)) 299 | 300 | process = Popen( 301 | command, 302 | env=env, 303 | stdout=PIPE, 304 | stderr=PIPE, 305 | close_fds=True, 306 | cwd=cwd, 307 | ) 308 | 309 | reader = _StreamReader(process.stdout, process.stderr) 310 | 311 | ret_stdout = [] if get_stdout else None 312 | ret_stderr = [] if get_stderr else None 313 | while True: 314 | item = reader.read(timeout=1) 315 | if item: 316 | stdout_line, stderr_line = item 317 | if stdout_line: 318 | if get_stdout: 319 | ret_stdout.append(stdout_line) 320 | if show_output: 321 | stdout.write(stdout_line.decode("utf-8", "replace")) 322 | stdout.flush() 323 | if stderr_line: 324 | if get_stderr: 325 | ret_stderr.append(stderr_line) 326 | if show_output: 327 | stderr.write(stderr_line.decode("utf-8", "replace")) 328 | stderr.flush() 329 | elif process.poll() is not None: 330 | # process has completed. 331 | break 332 | elif run_condition and not run_condition(): 333 | # time to terminate the process. 334 | process.terminate() 335 | # keep looping to get the rest of the output. 336 | 337 | if process.returncode != 0 and break_on_error: 338 | _command_fail(command, env, process.returncode) 339 | 340 | ret_stdout = b"".join(ret_stdout).decode("utf-8", "ignore") if ret_stdout else None 341 | ret_stderr = b"".join(ret_stderr).decode("utf-8", "ignore") if ret_stderr else None 342 | 343 | return CommandResult(ret_stdout, ret_stderr, process.returncode) 344 | 345 | 346 | def _command_fail(command, env, returncode): 347 | LOGGER.error("Command failed: {0}".format(command)) 348 | LOGGER.error("Error code: {0}".format(returncode)) 349 | LOGGER.log_env(LOGGER.ERROR, env) 350 | LOGGER.error("") 351 | LOGGER.error("Buildozer failed to execute the last command") 352 | if LOGGER.log_level <= LOGGER.INFO: 353 | LOGGER.error("If the error is not obvious, please raise the log_level to 2") 354 | LOGGER.error("and retry the latest command.") 355 | else: 356 | LOGGER.error("The error might be hidden in the log above this error") 357 | LOGGER.error("Please read the full log, and search for it before") 358 | LOGGER.error("raising an issue with buildozer itself.") 359 | LOGGER.error("In case of a bug report, please add a full log with log_level = 2") 360 | raise BuildozerCommandException() 361 | 362 | 363 | def cmd_expect(command, env, **kwargs): 364 | """ 365 | Launch a subprocess, returning a Pexpect instance that can be 366 | interacted with. 367 | """ 368 | # prepare the process 369 | kwargs.setdefault("show_output", LOGGER.log_level > 1) 370 | sensible = kwargs.pop("sensible", False) 371 | show_output = kwargs.pop("show_output") 372 | 373 | if show_output: 374 | kwargs["logfile"] = codecs.getwriter("utf8")(stdout.buffer) 375 | 376 | if not sensible: 377 | LOGGER.debug("Run (expect) {0!r}".format(command)) 378 | else: 379 | LOGGER.debug("Run (expect) {0!r} ...".format(command.split()[0])) 380 | 381 | LOGGER.debug("Cwd {}".format(kwargs.get("cwd"))) 382 | 383 | assert platform != "win32", "pexpect.spawn is not available on Windows." 384 | return pexpect.spawn(shlex.join(command), env=env, encoding="utf-8", **kwargs) 385 | 386 | 387 | def _report_download_progress(bytes_read, total_size): 388 | if total_size <= 0: # Sometimes we don't get told. 389 | progression = "{0} bytes".format(bytes_read) 390 | else: 391 | progression = "{0:.2f}%".format(100.0 * bytes_read / total_size) 392 | if "CI" not in os.environ: 393 | # Write over and over on same line. 394 | stdout.write("- Download {}\r".format(progression)) 395 | stdout.flush() 396 | 397 | 398 | def download(url, filename, cwd=None): 399 | """Download the file at url/filename to filename""" 400 | url = url + str(filename) 401 | 402 | LOGGER.debug("Downloading {0}".format(url)) 403 | 404 | if cwd: 405 | filename = join(cwd, filename) 406 | file_remove(filename) 407 | 408 | request = Request( 409 | url, 410 | headers={ 411 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " 412 | "(KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36" 413 | }, 414 | ) 415 | 416 | with urlopen(request) as response: 417 | total_size = int(response.headers.get("Content-Length", 0)) 418 | block_size = 1024 * 1024 # 1 MB 419 | bytes_read = 0 420 | 421 | with open(filename, "wb") as out_file: 422 | # Read in blocks, so we can give a progress bar. 423 | while True: 424 | block = response.read(block_size) 425 | if not block: 426 | break 427 | out_file.write(block) 428 | bytes_read += len(block) 429 | 430 | _report_download_progress(bytes_read, total_size) 431 | 432 | return filename 433 | -------------------------------------------------------------------------------- /buildozer/exceptions.py: -------------------------------------------------------------------------------- 1 | class BuildozerException(Exception): 2 | """ 3 | Exception raised for general situations buildozer cannot process. 4 | """ 5 | 6 | pass 7 | 8 | 9 | class BuildozerCommandException(BuildozerException): 10 | """ 11 | Exception raised when an external command failed. 12 | 13 | See: `Buildozer.buildops.cmd()`. 14 | """ 15 | 16 | pass 17 | -------------------------------------------------------------------------------- /buildozer/jsonstore.py: -------------------------------------------------------------------------------- 1 | """ 2 | Replacement for shelve, using json. 3 | This was needed to correctly support db between Python 2 and 3. 4 | """ 5 | 6 | __all__ = ["JsonStore"] 7 | 8 | import io 9 | from json import load, dump 10 | from os.path import exists 11 | 12 | 13 | class JsonStore: 14 | 15 | def __init__(self, filename): 16 | self.filename = filename 17 | self.data = {} 18 | if exists(filename): 19 | try: 20 | with io.open(filename, encoding='utf-8') as fd: 21 | self.data = load(fd) 22 | except ValueError: 23 | print("Unable to read the state.db, content will be replaced.") 24 | 25 | def __getitem__(self, key): 26 | return self.data[key] 27 | 28 | def __setitem__(self, key, value): 29 | self.data[key] = value 30 | self.sync() 31 | 32 | def __delitem__(self, key): 33 | del self.data[key] 34 | self.sync() 35 | 36 | def __contains__(self, item): 37 | return item in self.data 38 | 39 | def get(self, item, default=None): 40 | return self.data.get(item, default) 41 | 42 | def keys(self): 43 | return self.data.keys() 44 | 45 | def sync(self): 46 | with open(self.filename, 'w') as fd: 47 | dump(self.data, fd, ensure_ascii=False) 48 | -------------------------------------------------------------------------------- /buildozer/libs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kivy/buildozer/abc2d7e66c8abe096a95ed58befe6617f7efdad0/buildozer/libs/__init__.py -------------------------------------------------------------------------------- /buildozer/libs/_structures.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Donald Stufft 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | class Infinity: 16 | 17 | def __repr__(self): 18 | return "Infinity" 19 | 20 | def __hash__(self): 21 | return hash(repr(self)) 22 | 23 | def __lt__(self, other): 24 | return False 25 | 26 | def __le__(self, other): 27 | return False 28 | 29 | def __eq__(self, other): 30 | return isinstance(other, self.__class__) 31 | 32 | def __ne__(self, other): 33 | return not isinstance(other, self.__class__) 34 | 35 | def __gt__(self, other): 36 | return True 37 | 38 | def __ge__(self, other): 39 | return True 40 | 41 | def __neg__(self): 42 | return NegativeInfinity 43 | 44 | 45 | Infinity = Infinity() 46 | 47 | 48 | class NegativeInfinity: 49 | 50 | def __repr__(self): 51 | return "-Infinity" 52 | 53 | def __hash__(self): 54 | return hash(repr(self)) 55 | 56 | def __lt__(self, other): 57 | return True 58 | 59 | def __le__(self, other): 60 | return True 61 | 62 | def __eq__(self, other): 63 | return isinstance(other, self.__class__) 64 | 65 | def __ne__(self, other): 66 | return not isinstance(other, self.__class__) 67 | 68 | def __gt__(self, other): 69 | return False 70 | 71 | def __ge__(self, other): 72 | return False 73 | 74 | def __neg__(self): 75 | return Infinity 76 | 77 | 78 | NegativeInfinity = NegativeInfinity() 79 | -------------------------------------------------------------------------------- /buildozer/libs/version.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Donald Stufft 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import collections 15 | import itertools 16 | import re 17 | 18 | from ._structures import Infinity 19 | 20 | 21 | __all__ = [ 22 | "parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN" 23 | ] 24 | 25 | 26 | _Version = collections.namedtuple( 27 | "_Version", 28 | ["epoch", "release", "dev", "pre", "post", "local"], 29 | ) 30 | 31 | 32 | def parse(version): 33 | """ 34 | Parse the given version string and return either a :class:`Version` object 35 | or a :class:`LegacyVersion` object depending on if the given version is 36 | a valid PEP 440 version or a legacy version. 37 | """ 38 | try: 39 | return Version(version) 40 | except InvalidVersion: 41 | return LegacyVersion(version) 42 | 43 | 44 | class InvalidVersion(ValueError): 45 | """ 46 | An invalid version was found, users should refer to PEP 440. 47 | """ 48 | 49 | 50 | class _BaseVersion: 51 | 52 | def __hash__(self): 53 | return hash(self._key) 54 | 55 | def __lt__(self, other): 56 | return self._compare(other, lambda s, o: s < o) 57 | 58 | def __le__(self, other): 59 | return self._compare(other, lambda s, o: s <= o) 60 | 61 | def __eq__(self, other): 62 | return self._compare(other, lambda s, o: s == o) 63 | 64 | def __ge__(self, other): 65 | return self._compare(other, lambda s, o: s >= o) 66 | 67 | def __gt__(self, other): 68 | return self._compare(other, lambda s, o: s > o) 69 | 70 | def __ne__(self, other): 71 | return self._compare(other, lambda s, o: s != o) 72 | 73 | def _compare(self, other, method): 74 | if not isinstance(other, _BaseVersion): 75 | return NotImplemented 76 | 77 | return method(self._key, other._key) 78 | 79 | 80 | class LegacyVersion(_BaseVersion): 81 | 82 | def __init__(self, version): 83 | self._version = str(version) 84 | self._key = _legacy_cmpkey(self._version) 85 | 86 | def __str__(self): 87 | return self._version 88 | 89 | def __repr__(self): 90 | return "".format(repr(str(self))) 91 | 92 | @property 93 | def public(self): 94 | return self._version 95 | 96 | @property 97 | def base_version(self): 98 | return self._version 99 | 100 | @property 101 | def local(self): 102 | return None 103 | 104 | @property 105 | def is_prerelease(self): 106 | return False 107 | 108 | @property 109 | def is_postrelease(self): 110 | return False 111 | 112 | 113 | _legacy_version_component_re = re.compile( 114 | r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE, 115 | ) 116 | 117 | _legacy_version_replacement_map = { 118 | "pre": "c", "preview": "c", "-": "final-", "rc": "c", "dev": "@", 119 | } 120 | 121 | 122 | def _parse_version_parts(s): 123 | for part in _legacy_version_component_re.split(s): 124 | part = _legacy_version_replacement_map.get(part, part) 125 | 126 | if not part or part == ".": 127 | continue 128 | 129 | if part[:1] in "0123456789": 130 | # pad for numeric comparison 131 | yield part.zfill(8) 132 | else: 133 | yield "*" + part 134 | 135 | # ensure that alpha/beta/candidate are before final 136 | yield "*final" 137 | 138 | 139 | def _legacy_cmpkey(version): 140 | # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch 141 | # greater than or equal to 0. This will effectively put the LegacyVersion, 142 | # which uses the defacto standard originally implemented by setuptools, 143 | # as before all PEP 440 versions. 144 | epoch = -1 145 | 146 | # This scheme is taken from pkg_resources.parse_version setuptools prior to 147 | # it's adoption of the packaging library. 148 | parts = [] 149 | for part in _parse_version_parts(version.lower()): 150 | if part.startswith("*"): 151 | # remove "-" before a prerelease tag 152 | if part < "*final": 153 | while parts and parts[-1] == "*final-": 154 | parts.pop() 155 | 156 | # remove trailing zeros from each series of numeric parts 157 | while parts and parts[-1] == "00000000": 158 | parts.pop() 159 | 160 | parts.append(part) 161 | parts = tuple(parts) 162 | 163 | return epoch, parts 164 | 165 | 166 | # Deliberately not anchored to the start and end of the string, to make it 167 | # easier for 3rd party code to reuse 168 | VERSION_PATTERN = r""" 169 | v? 170 | (?: 171 | (?:(?P[0-9]+)!)? # epoch 172 | (?P[0-9]+(?:\.[0-9]+)*) # release segment 173 | (?P
                                          # pre-release
174 |             [-_\.]?
175 |             (?P(a|b|c|rc|alpha|beta|pre|preview))
176 |             [-_\.]?
177 |             (?P[0-9]+)?
178 |         )?
179 |         (?P                                         # post release
180 |             (?:-(?P[0-9]+))
181 |             |
182 |             (?:
183 |                 [-_\.]?
184 |                 (?Ppost|rev|r)
185 |                 [-_\.]?
186 |                 (?P[0-9]+)?
187 |             )
188 |         )?
189 |         (?P                                          # dev release
190 |             [-_\.]?
191 |             (?Pdev)
192 |             [-_\.]?
193 |             (?P[0-9]+)?
194 |         )?
195 |     )
196 |     (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
197 | """
198 | 
199 | 
200 | class Version(_BaseVersion):
201 | 
202 |     _regex = re.compile(
203 |         r"^\s*" + VERSION_PATTERN + r"\s*$",
204 |         re.VERBOSE | re.IGNORECASE,
205 |     )
206 | 
207 |     def __init__(self, version):
208 |         # Validate the version and parse it into pieces
209 |         match = self._regex.search(version)
210 |         if not match:
211 |             raise InvalidVersion("Invalid version: '{0}'".format(version))
212 | 
213 |         # Store the parsed out pieces of the version
214 |         self._version = _Version(
215 |             epoch=int(match.group("epoch")) if match.group("epoch") else 0,
216 |             release=tuple(int(i) for i in match.group("release").split(".")),
217 |             pre=_parse_letter_version(
218 |                 match.group("pre_l"),
219 |                 match.group("pre_n"),
220 |             ),
221 |             post=_parse_letter_version(
222 |                 match.group("post_l"),
223 |                 match.group("post_n1") or match.group("post_n2"),
224 |             ),
225 |             dev=_parse_letter_version(
226 |                 match.group("dev_l"),
227 |                 match.group("dev_n"),
228 |             ),
229 |             local=_parse_local_version(match.group("local")),
230 |         )
231 | 
232 |         # Generate a key which will be used for sorting
233 |         self._key = _cmpkey(
234 |             self._version.epoch,
235 |             self._version.release,
236 |             self._version.pre,
237 |             self._version.post,
238 |             self._version.dev,
239 |             self._version.local,
240 |         )
241 | 
242 |     def __repr__(self):
243 |         return "".format(repr(str(self)))
244 | 
245 |     def __str__(self):
246 |         parts = []
247 | 
248 |         # Epoch
249 |         if self._version.epoch != 0:
250 |             parts.append("{0}!".format(self._version.epoch))
251 | 
252 |         # Release segment
253 |         parts.append(".".join(str(x) for x in self._version.release))
254 | 
255 |         # Pre-release
256 |         if self._version.pre is not None:
257 |             parts.append("-" + "".join(str(x) for x in self._version.pre))
258 | 
259 |         # Post-release
260 |         if self._version.post is not None:
261 |             parts.append(".post{0}".format(self._version.post[1]))
262 | 
263 |         # Development release
264 |         if self._version.dev is not None:
265 |             parts.append(".dev{0}".format(self._version.dev[1]))
266 | 
267 |         # Local version segment
268 |         if self._version.local is not None:
269 |             parts.append(
270 |                 "+{0}".format(".".join(str(x) for x in self._version.local))
271 |             )
272 | 
273 |         return "".join(parts)
274 | 
275 |     @property
276 |     def public(self):
277 |         return str(self).split("+", 1)[0]
278 | 
279 |     @property
280 |     def base_version(self):
281 |         parts = []
282 | 
283 |         # Epoch
284 |         if self._version.epoch != 0:
285 |             parts.append("{0}!".format(self._version.epoch))
286 | 
287 |         # Release segment
288 |         parts.append(".".join(str(x) for x in self._version.release))
289 | 
290 |         return "".join(parts)
291 | 
292 |     @property
293 |     def local(self):
294 |         version_string = str(self)
295 |         if "+" in version_string:
296 |             return version_string.split("+", 1)[1]
297 | 
298 |     @property
299 |     def is_prerelease(self):
300 |         return bool(self._version.dev or self._version.pre)
301 | 
302 |     @property
303 |     def is_postrelease(self):
304 |         return bool(self._version.post)
305 | 
306 | 
307 | def _parse_letter_version(letter, number):
308 |     if letter:
309 |         # We consider there to be an implicit 0 in a pre-release if there is
310 |         # not a numeral associated with it.
311 |         if number is None:
312 |             number = 0
313 | 
314 |         # We normalize any letters to their lower case form
315 |         letter = letter.lower()
316 | 
317 |         # We consider some words to be alternate spellings of other words and
318 |         # in those cases we want to normalize the spellings to our preferred
319 |         # spelling.
320 |         if letter == "alpha":
321 |             letter = "a"
322 |         elif letter == "beta":
323 |             letter = "b"
324 |         elif letter in ["c", "pre", "preview"]:
325 |             letter = "rc"
326 | 
327 |         return letter, int(number)
328 |     if not letter and number:
329 |         # We assume if we are given a number, but we are not given a letter
330 |         # then this is using the implicit post release syntax (e.g. 1.0-1)
331 |         letter = "post"
332 | 
333 |         return letter, int(number)
334 | 
335 | 
336 | _local_version_separators = re.compile(r"[\._-]")
337 | 
338 | 
339 | def _parse_local_version(local):
340 |     """
341 |     Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
342 |     """
343 |     if local is not None:
344 |         return tuple(
345 |             part.lower() if not part.isdigit() else int(part)
346 |             for part in _local_version_separators.split(local)
347 |         )
348 | 
349 | 
350 | def _cmpkey(epoch, release, pre, post, dev, local):
351 |     # When we compare a release version, we want to compare it with all of the
352 |     # trailing zeros removed. So we'll use a reverse the list, drop all the now
353 |     # leading zeros until we come to something non zero, then take the rest
354 |     # re-reverse it back into the correct order and make it a tuple and use
355 |     # that for our sorting key.
356 |     release = tuple(
357 |         reversed(list(
358 |             itertools.dropwhile(
359 |                 lambda x: x == 0,
360 |                 reversed(release),
361 |             )
362 |         ))
363 |     )
364 | 
365 |     # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
366 |     # We'll do this by abusing the pre segment, but we _only_ want to do this
367 |     # if there is not a pre or a post segment. If we have one of those then
368 |     # the normal sorting rules will handle this case correctly.
369 |     if pre is None and post is None and dev is not None:
370 |         pre = -Infinity
371 |     # Versions without a pre-release (except as noted above) should sort after
372 |     # those with one.
373 |     elif pre is None:
374 |         pre = Infinity
375 | 
376 |     # Versions without a post segment should sort before those with one.
377 |     if post is None:
378 |         post = -Infinity
379 | 
380 |     # Versions without a development segment should sort after those with one.
381 |     if dev is None:
382 |         dev = Infinity
383 | 
384 |     if local is None:
385 |         # Versions without a local segment should sort before those with one.
386 |         local = -Infinity
387 |     else:
388 |         # Versions with a local segment need that segment parsed to implement
389 |         # the sorting rules in PEP440.
390 |         # - Alpha numeric segments sort before numeric segments
391 |         # - Alpha numeric segments sort lexicographically
392 |         # - Numeric segments sort numerically
393 |         # - Shorter versions sort before longer versions when the prefixes
394 |         #   match exactly
395 |         local = tuple(
396 |             (i, "") if isinstance(i, int) else (-Infinity, i)
397 |             for i in local
398 |         )
399 | 
400 |     return epoch, release, pre, post, dev, local
401 | 


--------------------------------------------------------------------------------
/buildozer/logger.py:
--------------------------------------------------------------------------------
 1 | """
 2 | Logger
 3 | ======
 4 | 
 5 | Logger implementation used by Buildozer.
 6 | 
 7 | Supports colored output, where available.
 8 | """
 9 | 
10 | from os import environ
11 | from pprint import pformat
12 | import sys
13 | 
14 | try:
15 |     # if installed, it can give color to Windows as well
16 |     import colorama
17 | 
18 |     colorama.init()
19 | except ImportError:
20 |     colorama = None
21 | 
22 | # set color codes
23 | if colorama:
24 |     RESET_SEQ = colorama.Fore.RESET + colorama.Style.RESET_ALL
25 |     COLOR_SEQ = lambda x: x  # noqa: E731
26 |     BOLD_SEQ = ""
27 |     if sys.platform == "win32":
28 |         BLACK = colorama.Fore.BLACK + colorama.Style.DIM
29 |     else:
30 |         BLACK = colorama.Fore.BLACK + colorama.Style.BRIGHT
31 |     RED = colorama.Fore.RED
32 |     BLUE = colorama.Fore.CYAN
33 |     USE_COLOR = "NO_COLOR" not in environ
34 | elif sys.platform != "win32":
35 |     RESET_SEQ = "\033[0m"
36 |     COLOR_SEQ = lambda x: "\033[1;{}m".format(30 + x)  # noqa: E731
37 |     BOLD_SEQ = "\033[1m"
38 |     BLACK = 0
39 |     RED = 1
40 |     BLUE = 4
41 |     USE_COLOR = "NO_COLOR" not in environ
42 | else:
43 |     RESET_SEQ = ""
44 |     COLOR_SEQ = ""
45 |     BOLD_SEQ = ""
46 |     RED = BLUE = BLACK = 0
47 |     USE_COLOR = False
48 | 
49 | 
50 | class Logger:
51 |     ERROR = 0
52 |     INFO = 1
53 |     DEBUG = 2
54 | 
55 |     LOG_LEVELS_C = (RED, BLUE, BLACK)  # Map levels to colors
56 |     LOG_LEVELS_T = "EID"  # error, info, debug
57 | 
58 |     log_level = ERROR
59 | 
60 |     def log(self, level, msg):
61 |         if level > self.log_level:
62 |             return
63 |         if USE_COLOR:
64 |             color = COLOR_SEQ(Logger.LOG_LEVELS_C[level])
65 |             print("".join((RESET_SEQ, color, "# ", msg, RESET_SEQ)))
66 |         else:
67 |             print("{} {}".format(Logger.LOG_LEVELS_T[level], msg))
68 | 
69 |     def debug(self, msg):
70 |         self.log(self.DEBUG, msg)
71 | 
72 |     def info(self, msg):
73 |         self.log(self.INFO, msg)
74 | 
75 |     def error(self, msg):
76 |         self.log(self.ERROR, msg)
77 | 
78 |     def log_env(self, level, env):
79 |         """dump env into logger in readable format"""
80 |         self.log(level, "ENVIRONMENT:")
81 |         for k, v in env.items():
82 |             self.log(level, "    {} = {}".format(k, pformat(v)))
83 | 
84 |     @classmethod
85 |     def set_level(cls, level):
86 |         """set minimum threshold for log messages"""
87 |         cls.log_level = level
88 | 


--------------------------------------------------------------------------------
/buildozer/scripts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kivy/buildozer/abc2d7e66c8abe096a95ed58befe6617f7efdad0/buildozer/scripts/__init__.py


--------------------------------------------------------------------------------
/buildozer/scripts/client.py:
--------------------------------------------------------------------------------
 1 | '''
 2 | Main Buildozer client
 3 | =====================
 4 | 
 5 | '''
 6 | 
 7 | import sys
 8 | 
 9 | from buildozer import Buildozer
10 | from buildozer.exceptions import BuildozerCommandException, BuildozerException
11 | from buildozer.logger import Logger
12 | 
13 | 
14 | def main():
15 |     try:
16 |         Buildozer().run_command(sys.argv[1:])
17 |     except BuildozerCommandException:
18 |         # don't show the exception in the command line. The log already show
19 |         # the command failed.
20 |         sys.exit(1)
21 |     except BuildozerException as error:
22 |         Logger().error('%s' % error)
23 |         sys.exit(1)
24 | 
25 | 
26 | if __name__ == '__main__':
27 |     main()
28 | 


--------------------------------------------------------------------------------
/buildozer/scripts/remote.py:
--------------------------------------------------------------------------------
  1 | '''
  2 | Buildozer remote
  3 | ================
  4 | 
  5 | .. warning::
  6 | 
  7 |     This is an experimental tool and not widely used. It might not fit for you.
  8 | 
  9 | Pack and send the source code to a remote SSH server, bundle buildozer with it,
 10 | and start the build on the remote.
 11 | You need paramiko to make it work.
 12 | '''
 13 | 
 14 | __all__ = ["BuildozerRemote"]
 15 | 
 16 | from configparser import ConfigParser
 17 | from os import makedirs, walk, getcwd
 18 | from os.path import join, expanduser, realpath, exists, splitext
 19 | import socket
 20 | from select import select
 21 | import sys
 22 | from sys import stdout, stdin, exit
 23 | try:
 24 |     import termios
 25 |     has_termios = True
 26 | except ImportError:
 27 |     has_termios = False
 28 | 
 29 | try:
 30 |     import paramiko
 31 | except ImportError:
 32 |     print('Paramiko missing: pip install paramiko')
 33 | 
 34 | from buildozer import Buildozer, __version__
 35 | from buildozer.exceptions import BuildozerCommandException, BuildozerException
 36 | from buildozer.logger import Logger
 37 | 
 38 | 
 39 | class BuildozerRemote(Buildozer):
 40 |     def run_command(self, args):
 41 |         profile = None
 42 | 
 43 |         while args:
 44 |             if not args[0].startswith('-'):
 45 |                 break
 46 |             arg = args.pop(0)
 47 | 
 48 |             if arg in ('-v', '--verbose'):
 49 |                 self.logger.log_level = 2
 50 | 
 51 |             elif arg in ('-p', '--profile'):
 52 |                 profile = args.pop(0)
 53 | 
 54 |             elif arg in ('-h', '--help'):
 55 |                 self.usage()
 56 |                 exit(0)
 57 | 
 58 |             elif arg == '--version':
 59 |                 print('Buildozer (remote) {0}'.format(__version__))
 60 |                 exit(0)
 61 | 
 62 |         self.config.apply_profile(profile)
 63 | 
 64 |         if len(args) < 2:
 65 |             self.usage()
 66 |             return
 67 | 
 68 |         remote_name = args[0]
 69 |         remote_section = 'remote:{}'.format(remote_name)
 70 |         if not self.config.has_section(remote_section):
 71 |             self.logger.error('Unknown remote "{}", must be configured first.'.format(
 72 |                 remote_name))
 73 |             return
 74 | 
 75 |         self.remote_host = remote_host = self.config.get(
 76 |                 remote_section, 'host', '')
 77 |         self.remote_port = self.config.get(
 78 |                 remote_section, 'port', '22')
 79 |         self.remote_user = remote_user = self.config.get(
 80 |                 remote_section, 'user', '')
 81 |         self.remote_build_dir = remote_build_dir = self.config.get(
 82 |                 remote_section, 'build_directory', '')
 83 |         self.remote_identity = self.config.get(
 84 |                 remote_section, 'identity', '')
 85 |         if not remote_host:
 86 |             self.logger.error('Missing "host = " for {}'.format(remote_section))
 87 |             return
 88 |         if not remote_user:
 89 |             self.logger.error('Missing "user = " for {}'.format(remote_section))
 90 |             return
 91 |         if not remote_build_dir:
 92 |             self.logger.error('Missing "build_directory = " for {}'.format(remote_section))
 93 |             return
 94 | 
 95 |         # fake the target
 96 |         self.targetname = 'remote'
 97 |         self.check_build_layout()
 98 | 
 99 |         # prepare our source code
100 |         self.logger.info('Prepare source code to sync')
101 |         self._copy_application_sources()
102 |         self._ssh_connect()
103 |         try:
104 |             self._ensure_buildozer()
105 |             self._sync_application_sources()
106 |             self._do_remote_commands(args[1:])
107 |             self._ssh_sync(getcwd(), mode='get')
108 |         finally:
109 |             self._ssh_close()
110 | 
111 |     def _ssh_connect(self):
112 |         self.logger.info('Connecting to {}'.format(self.remote_host))
113 |         self._ssh_client = client = paramiko.SSHClient()
114 |         client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
115 |         client.load_system_host_keys()
116 |         kwargs = {}
117 |         if self.remote_identity:
118 |             kwargs['key_filename'] = expanduser(self.remote_identity)
119 |         client.connect(self.remote_host, username=self.remote_user,
120 |                 port=int(self.remote_port), **kwargs)
121 |         self._sftp_client = client.open_sftp()
122 | 
123 |     def _ssh_close(self):
124 |         self.logger.debug('Closing remote connection')
125 |         self._sftp_client.close()
126 |         self._ssh_client.close()
127 | 
128 |     def _ensure_buildozer(self):
129 |         s = self._sftp_client
130 |         root_dir = s.normalize('.')
131 |         self.remote_build_dir = join(root_dir, self.remote_build_dir,
132 |                 self.package_full_name)
133 |         self.logger.debug('Remote build directory: {}'.format(self.remote_build_dir))
134 |         self._ssh_mkdir(self.remote_build_dir)
135 |         self._ssh_sync(__path__[0])  # noqa: F821 undefined name
136 | 
137 |     def _sync_application_sources(self):
138 |         self.logger.info('Synchronize application sources')
139 |         self._ssh_sync(self.app_dir)
140 | 
141 |         # create custom buildozer.spec
142 |         self.logger.info('Create custom buildozer.spec')
143 |         config = ConfigParser()
144 |         config.read('buildozer.spec')
145 |         config.set('app', 'source.dir', 'app')
146 | 
147 |         fn = join(self.remote_build_dir, 'buildozer.spec')
148 |         fd = self._sftp_client.open(fn, 'wb')
149 |         config.write(fd)
150 |         fd.close()
151 | 
152 |     def _do_remote_commands(self, args):
153 |         self.logger.info('Execute remote buildozer')
154 |         cmd = (
155 |             'source ~/.profile;'
156 |             'cd {0};'
157 |             'env PYTHONPATH={0}:$PYTHONPATH '
158 |             'python -c "import buildozer, sys;'
159 |             'buildozer.Buildozer().run_command(sys.argv[1:])" {1} {2} 2>&1').format(
160 |             self.remote_build_dir,
161 |             '--verbose' if self.logger.log_level == 2 else '',
162 |             ' '.join(args),
163 |         )
164 |         self._ssh_command(cmd)
165 | 
166 |     def _ssh_mkdir(self, *args):
167 |         directory = join(*args)
168 |         self.logger.debug('Create remote directory {}'.format(directory))
169 |         try:
170 |             self._sftp_client.mkdir(directory)
171 |         except IOError:
172 |             # already created?
173 |             try:
174 |                 self._sftp_client.stat(directory)
175 |             except IOError:
176 |                 self.logger.error('Unable to create remote directory {}'.format(directory))
177 |                 raise
178 | 
179 |     def _ssh_sync(self, directory, mode='put'):
180 |         self.logger.debug('Syncing {} directory'.format(directory))
181 |         directory = realpath(expanduser(directory))
182 |         base_strip = directory.rfind('/')
183 |         if mode == 'get':
184 |             local_dir = join(directory, 'bin')
185 |             remote_dir = join(self.remote_build_dir, 'bin')
186 |             if not exists(local_dir):
187 |                 makedirs(local_dir)
188 |             for _file in self._sftp_client.listdir(path=remote_dir):
189 |                 self._sftp_client.get(join(remote_dir, _file),
190 |                                       join(local_dir, _file))
191 |             return
192 |         for root, dirs, files in walk(directory):
193 |             self._ssh_mkdir(self.remote_build_dir, root[base_strip + 1:])
194 |             for fn in files:
195 |                 if splitext(fn)[1] in ('.pyo', '.pyc', '.swp'):
196 |                     continue
197 |                 local_file = join(root, fn)
198 |                 remote_file = join(self.remote_build_dir, root[base_strip + 1:], fn)
199 |                 self.logger.debug('Sync {} -> {}'.format(local_file, remote_file))
200 |                 self._sftp_client.put(local_file, remote_file)
201 | 
202 |     def _ssh_command(self, command):
203 |         self.logger.debug('Execute remote command {}'.format(command))
204 |         transport = self._ssh_client.get_transport()
205 |         channel = transport.open_session()
206 |         try:
207 |             channel.exec_command(command)
208 |             self._interactive_shell(channel)
209 |         finally:
210 |             channel.close()
211 | 
212 |     def _interactive_shell(self, chan):
213 |         if has_termios:
214 |             self._posix_shell(chan)
215 |         else:
216 |             self._windows_shell(chan)
217 | 
218 |     def _posix_shell(self, chan):
219 |         oldtty = termios.tcgetattr(stdin)
220 |         try:
221 |             chan.settimeout(0.0)
222 | 
223 |             while True:
224 |                 r, w, e = select([chan, stdin], [], [])
225 |                 if chan in r:
226 |                     try:
227 |                         x = chan.recv(128)
228 |                         if len(x) == 0:
229 |                             print('\r\n*** EOF\r\n',)
230 |                             break
231 |                         stdout.write(x)
232 |                         stdout.flush()
233 |                     except socket.timeout:
234 |                         pass
235 |                 if stdin in r:
236 |                     x = stdin.read(1)
237 |                     if len(x) == 0:
238 |                         break
239 |                     chan.sendall(x)
240 |         finally:
241 |             termios.tcsetattr(stdin, termios.TCSADRAIN, oldtty)
242 | 
243 |     # thanks to Mike Looijmans for this code
244 |     def _windows_shell(self, chan):
245 |         import threading
246 | 
247 |         stdout.write("Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n")
248 | 
249 |         def writeall(sock):
250 |             while True:
251 |                 data = sock.recv(256)
252 |                 if not data:
253 |                     stdout.write('\r\n*** EOF ***\r\n\r\n')
254 |                     stdout.flush()
255 |                     break
256 |                 stdout.write(data)
257 |                 stdout.flush()
258 | 
259 |         writer = threading.Thread(target=writeall, args=(chan,))
260 |         writer.start()
261 | 
262 |         try:
263 |             while True:
264 |                 d = stdin.read(1)
265 |                 if not d:
266 |                     break
267 |                 chan.send(d)
268 |         except EOFError:
269 |             # user hit ^Z or F6
270 |             pass
271 | 
272 | 
273 | def main():
274 |     try:
275 |         BuildozerRemote().run_command(sys.argv[1:])
276 |     except BuildozerCommandException:
277 |         pass
278 |     except BuildozerException as error:
279 |         Logger().error('%s' % error)
280 | 
281 | 
282 | if __name__ == '__main__':
283 |     main()
284 | 


--------------------------------------------------------------------------------
/buildozer/sitecustomize.py:
--------------------------------------------------------------------------------
1 | from os.path import join, dirname
2 | import sys
3 | sys.path.append(join(dirname(__file__), '_applibs'))
4 | 


--------------------------------------------------------------------------------
/buildozer/specparser.py:
--------------------------------------------------------------------------------
  1 | """
  2 |     A customised ConfigParser, suitable for buildozer.spec.
  3 | 
  4 |     Supports
  5 |         - list values
  6 |             - either comma separated, or in their own [section:option] section.
  7 |         - environment variable overrides of values
  8 |             - overrides applied at construction.
  9 |         - profiles
 10 |         - case-sensitive keys
 11 |         - "No values" are permitted.
 12 | """
 13 | 
 14 | from configparser import ConfigParser
 15 | from os import environ
 16 | 
 17 | from buildozer.logger import Logger
 18 | 
 19 | 
 20 | class SpecParser(ConfigParser):
 21 |     def __init__(self, *args, **kwargs):
 22 |         # Allow "no value" options to better support lists.
 23 |         super().__init__(*args, allow_no_value=True, **kwargs)
 24 | 
 25 |     def optionxform(self, optionstr: str) -> str:
 26 |         """Override method that canonicalizes keys to retain
 27 |         case sensitivity."""
 28 |         return optionstr
 29 | 
 30 |     # Override all the readers to apply env variables over the top.
 31 | 
 32 |     def read(self, filenames, encoding=None):
 33 |         super().read(filenames, encoding)
 34 |         # Let environment variables override the values
 35 |         self._override_config_from_envs()
 36 | 
 37 |     def read_file(self, f, source=None):
 38 |         super().read_file(f, source)
 39 |         # Let environment variables override the values
 40 |         self._override_config_from_envs()
 41 | 
 42 |     def read_string(self, string, source=""):
 43 |         super().read_string(string, source)
 44 |         # Let environment variables override the values
 45 |         self._override_config_from_envs()
 46 | 
 47 |     def read_dict(self, dictionary, source=""):
 48 |         super().read_dict(dictionary, source)
 49 |         # Let environment variables override the values
 50 |         self._override_config_from_envs()
 51 | 
 52 |     # Add new getters
 53 | 
 54 |     def getlist(
 55 |         self,
 56 |         section,
 57 |         token,
 58 |         default=None,
 59 |         with_values=False,
 60 |         strip=True,
 61 |         section_sep="=",
 62 |         split_char=",",
 63 |     ):
 64 |         """Return a list of strings.
 65 | 
 66 |         They can be found as the list of options in a [section:token] section,
 67 |         or in a [section], under the a option, as a comma-separated (or
 68 |         split_char-separated) list,
 69 |         Failing that, default is returned (as is).
 70 | 
 71 |         If with_values is set, and they are in a [section:token] section,
 72 |         the option values are included with the option key,
 73 |         separated by section_sep
 74 |         """
 75 | 
 76 |         # if a section:token is defined, let's use the content as a list.
 77 |         l_section = "{}:{}".format(section, token)
 78 |         if self.has_section(l_section):
 79 |             values = self.options(l_section)
 80 |             if with_values:
 81 |                 return [
 82 |                     "{}{}{}".format(key, section_sep, self.get(l_section, key))
 83 |                     for key in values
 84 |                 ]
 85 |             return values if not strip else [x.strip() for x in values]
 86 |         values = self.getdefault(section, token, None)
 87 |         if values is None:
 88 |             return default
 89 |         values = values.split(split_char)
 90 |         if not values:
 91 |             return default
 92 |         return values if not strip else [x.strip() for x in values]
 93 | 
 94 |     def getlistvalues(self, section, token, default=None):
 95 |         """Convenience function.
 96 |         Deprecated - call getlist directly."""
 97 |         return self.getlist(section, token, default, with_values=True)
 98 | 
 99 |     def getdefault(self, section, token, default=None):
100 |         """
101 |         Convenience function.
102 |         Deprecated - call get directly."""
103 |         return self.get(section, token, fallback=default)
104 | 
105 |     def getbooldefault(self, section, token, default=False):
106 |         """
107 |         Convenience function.
108 |         Deprecated - call getboolean directly."""
109 |         return self.getboolean(section, token, fallback=default)
110 | 
111 |     def apply_profile(self, profile):
112 |         """
113 |         Sections marked with an @ followed by a list of profiles are only
114 |         applied if the profile is provided here.
115 | 
116 |         Implementation Note: A better structure would be for the Profile to be
117 |         provided in the constructor, so this could be a private method
118 |         automatically applied on read *before* _override_config_from_envs(),
119 |         but that will require a significant restructure of Buildozer.
120 | 
121 |         Instead, this must be called by the client after the read, and the env
122 |         var overrides need to be reapplied to the relevant options.
123 |         """
124 |         if not profile:
125 |             return
126 |         for section in self.sections():
127 | 
128 |             # extract the profile part from the section name
129 |             # example: [app@default,hd]
130 |             parts = section.split("@", 1)
131 |             if len(parts) < 2:
132 |                 continue
133 | 
134 |             # create a list that contain all the profiles of the current section
135 |             # ['default', 'hd']
136 |             section_base, section_profiles = parts
137 |             section_profiles = section_profiles.split(",")
138 | 
139 |             # Trim
140 |             section_base = section_base.strip()
141 |             section_profiles = [profile.strip() for profile in section_profiles]
142 | 
143 |             if profile not in section_profiles:
144 |                 continue
145 | 
146 |             # the current profile is one available in the section
147 |             # merge with the general section, or make it one.
148 |             if not self.has_section(section_base):
149 |                 self.add_section(section_base)
150 |             for name, value in self.items(section):
151 |                 Logger().debug(
152 |                     "merged ({}, {}) into {} (profile is {})".format(
153 |                         name, value, section_base, profile
154 |                     )
155 |                 )
156 |                 self.set(section_base, name, value)
157 | 
158 |                 # Reapply env var, if any.
159 |                 self._override_config_token_from_env(section_base, name)
160 | 
161 |     def _override_config_from_envs(self):
162 |         """Takes a ConfigParser, and checks every section/token for an
163 |         environment variable of the form SECTION_TOKEN, with any dots
164 |         replaced by underscores. If the variable exists, sets the config
165 |         variable to the env value.
166 |         """
167 |         for section in self.sections():
168 |             for token in self.options(section):
169 |                 self._override_config_token_from_env(section, token)
170 | 
171 |     def _override_config_token_from_env(self, section, token):
172 |         """Given a config section and token, checks for an appropriate
173 |         environment variable. If the variable exists, sets the config entry to
174 |         its value.
175 | 
176 |         The environment variable checked is of the form SECTION_TOKEN, all
177 |         upper case, with any dots replaced by underscores.
178 | 
179 |         """
180 |         env_var_name = "_".join(
181 |             item.upper().replace(".", "_") for item in (section, token)
182 |         )
183 |         env_var = environ.get(env_var_name)
184 |         if env_var is not None:
185 |             self.set(section, token, env_var)
186 | 


--------------------------------------------------------------------------------
/buildozer/target.py:
--------------------------------------------------------------------------------
  1 | from sys import exit
  2 | import os
  3 | from os.path import join
  4 | 
  5 | import buildozer.buildops as buildops
  6 | from buildozer.logger import Logger
  7 | 
  8 | 
  9 | def no_config(f):
 10 |     f.__no_config = True
 11 |     return f
 12 | 
 13 | 
 14 | class Target:
 15 |     def __init__(self, buildozer):
 16 |         self.buildozer = buildozer
 17 |         self.build_mode = 'debug'
 18 |         self.platform_update = False
 19 |         self.logger = Logger()
 20 | 
 21 |     def check_requirements(self):
 22 |         pass
 23 | 
 24 |     def check_configuration_tokens(self, errors=None):
 25 |         if errors:
 26 |             self.logger.info('Check target configuration tokens')
 27 |             self.logger.error(
 28 |                 '{0} error(s) found in the buildozer.spec'.format(
 29 |                     len(errors)))
 30 |             for error in errors:
 31 |                 print(error)
 32 |             exit(1)
 33 | 
 34 |     def compile_platform(self):
 35 |         pass
 36 | 
 37 |     def install_platform(self):
 38 |         pass
 39 | 
 40 |     def get_custom_commands(self):
 41 |         result = []
 42 |         for x in dir(self):
 43 |             if not x.startswith('cmd_'):
 44 |                 continue
 45 |             if x[4:] in self.buildozer.standard_cmds:
 46 |                 continue
 47 |             result.append((x[4:], getattr(self, x).__doc__))
 48 |         return result
 49 | 
 50 |     def get_available_packages(self):
 51 |         return ['kivy']
 52 | 
 53 |     def run_commands(self, args):
 54 |         if not args:
 55 |             self.logger.error('Missing target command')
 56 |             self.buildozer.usage()
 57 |             exit(1)
 58 | 
 59 |         result = []
 60 |         last_command = []
 61 |         while args:
 62 |             arg = args.pop(0)
 63 |             if arg == '--':
 64 |                 if last_command:
 65 |                     last_command += args
 66 |                     break
 67 |             elif not arg.startswith('--'):
 68 |                 if last_command:
 69 |                     result.append(last_command)
 70 |                     last_command = []
 71 |                 last_command.append(arg)
 72 |             else:
 73 |                 if not last_command:
 74 |                     self.logger.error('Argument passed without a command')
 75 |                     self.buildozer.usage()
 76 |                     exit(1)
 77 |                 last_command.append(arg)
 78 |         if last_command:
 79 |             result.append(last_command)
 80 | 
 81 |         config_check = False
 82 | 
 83 |         for item in result:
 84 |             command, args = item[0], item[1:]
 85 |             if not hasattr(self, 'cmd_{0}'.format(command)):
 86 |                 self.logger.error('Unknown command {0}'.format(command))
 87 |                 exit(1)
 88 | 
 89 |             func = getattr(self, 'cmd_{0}'.format(command))
 90 | 
 91 |             need_config_check = not hasattr(func, '__no_config')
 92 |             if need_config_check and not config_check:
 93 |                 config_check = True
 94 |                 self.check_configuration_tokens()
 95 | 
 96 |             func(args)
 97 | 
 98 |     def cmd_clean(self, *args):
 99 |         self.buildozer.clean_platform()
100 | 
101 |     def cmd_update(self, *args):
102 |         self.platform_update = True
103 |         self.buildozer.prepare_for_build()
104 | 
105 |     def cmd_debug(self, *args):
106 |         self.buildozer.prepare_for_build()
107 |         self.build_mode = 'debug'
108 |         self.buildozer.build()
109 | 
110 |     def cmd_release(self, *args):
111 |         error = self.logger.error
112 |         self.buildozer.prepare_for_build()
113 |         if self.buildozer.config.get("app", "package.domain") == "org.test":
114 |             error("")
115 |             error("ERROR: Trying to release a package that starts with org.test")
116 |             error("")
117 |             error("The package.domain org.test is, as the name intented, a test.")
118 |             error("Once you published an application with org.test,")
119 |             error("you cannot change it, it will be part of the identifier")
120 |             error("for Google Play / App Store / etc.")
121 |             error("")
122 |             error("So change package.domain to anything else.")
123 |             error("")
124 |             error("If you messed up before, set the environment variable to force the build:")
125 |             error("export BUILDOZER_ALLOW_ORG_TEST_DOMAIN=1")
126 |             error("")
127 |             if "BUILDOZER_ALLOW_ORG_TEST_DOMAIN" not in os.environ:
128 |                 exit(1)
129 | 
130 |         if self.buildozer.config.get("app", "package.domain") == "org.kivy":
131 |             error("")
132 |             error("ERROR: Trying to release a package that starts with org.kivy")
133 |             error("")
134 |             error("The package.domain org.kivy is reserved for the Kivy official")
135 |             error("applications. Please use your own domain.")
136 |             error("")
137 |             error("If you are a Kivy developer, add an export in your shell")
138 |             error("export BUILDOZER_ALLOW_KIVY_ORG_DOMAIN=1")
139 |             error("")
140 |             if "BUILDOZER_ALLOW_KIVY_ORG_DOMAIN" not in os.environ:
141 |                 exit(1)
142 | 
143 |         self.build_mode = 'release'
144 |         self.buildozer.build()
145 | 
146 |     def cmd_deploy(self, *args):
147 |         self.buildozer.prepare_for_build()
148 | 
149 |     def cmd_run(self, *args):
150 |         self.buildozer.prepare_for_build()
151 | 
152 |     def cmd_serve(self, *args):
153 |         self.buildozer.cmd_serve()
154 | 
155 |     def path_or_git_url(self, repo, owner='kivy', branch='master',
156 |                         url_format='https://github.com/{owner}/{repo}.git',
157 |                         platform=None,
158 |                         squash_hyphen=True):
159 |         """Get source location for a git checkout
160 | 
161 |         This method will check the `buildozer.spec` for the keys:
162 |             {repo}_dir
163 |             {repo}_url
164 |             {repo}_branch
165 | 
166 |         and use them to determine the source location for a git checkout.
167 | 
168 |         If a `platform` is specified, {platform}.{repo} will be used
169 |         as the base for the buildozer key
170 | 
171 |         `{repo}_dir` specifies a custom checkout location
172 |         (relative to `buildozer.root_dir`). If present, `path` will be
173 |         set to this value and `url`, `branch` will be set to None,
174 |         None. Otherwise, `{repo}_url` and `{repo}_branch` will be
175 |         examined.
176 | 
177 |         If no keys are present, the kwargs will be used to create
178 |         a sensible default URL and branch.
179 | 
180 |         :Parameters:
181 |             `repo`: str (required)
182 |                 name of repository to fetch. Used both for buildozer
183 |                 keys ({platform}.{repo}_dir|_url|_branch) and in building
184 |                 default git URL
185 |             `branch`: str (default 'master')
186 |                 Specific branch to retrieve if none specified in
187 |                 buildozer.spec.
188 |             `owner`: str
189 |                 owner of repo.
190 |             `platform`: str or None
191 |                 platform prefix to use when retrieving `buildozer.spec`
192 |                 keys. If specified, key names will be {platform}.{repo}
193 |                 instead of just {repo}
194 |             `squash_hyphen`: boolean
195 |                 if True, change '-' to '_' when looking for
196 |                 keys in buildozer.spec. This lets us keep backwards
197 |                 compatibility with old buildozer.spec files
198 |             `url_format`: format string
199 |                 Used to construct default git URL.
200 |                 can use {repo} {owner} and {branch} if needed.
201 | 
202 |         :Returns:
203 |             A Tuple (path, url, branch) where
204 |                 `path`
205 |                     Path to a custom git checkout. If specified,
206 |                     both `url` and `branch` will be None
207 |                 `url`
208 |                     URL of git repository from where code should be
209 |                     checked-out
210 |                 `branch`
211 |                     branch name (or tag) that should be used for the
212 |                     check-out.
213 | 
214 |         """
215 |         if squash_hyphen:
216 |             key = repo.replace('-', '_')
217 |         else:
218 |             key = repo
219 |         if platform:
220 |             key = "{}.{}".format(platform, key)
221 |         config = self.buildozer.config
222 |         path = config.getdefault('app', '{}_dir'.format(key), None)
223 | 
224 |         if path is not None:
225 |             path = join(self.buildozer.root_dir, path)
226 |             url = None
227 |             branch = None
228 |         else:
229 |             branch = config.getdefault('app', '{}_branch'.format(key), branch)
230 |             default_url = url_format.format(owner=owner, repo=repo, branch=branch)
231 |             url = config.getdefault('app', '{}_url'.format(key), default_url)
232 |         return path, url, branch
233 | 
234 |     def install_or_update_repo(self, repo, **kwargs):
235 |         """Install or update a git repository into the platform directory.
236 | 
237 |         This will clone the contents of a git repository to
238 |         `buildozer.platform_dir`. The location of this repo can be
239 |         specified via URL and branch name, or via a custom (local)
240 |         directory name.
241 | 
242 |         :Parameters:
243 |             **kwargs:
244 |                 Any valid arguments for :meth:`path_or_git_url`
245 | 
246 |         :Returns:
247 |             fully qualified path to updated git repo
248 |         """
249 |         install_dir = join(self.buildozer.platform_dir, repo)
250 |         custom_dir, clone_url, clone_branch = self.path_or_git_url(repo, **kwargs)
251 |         if not buildops.file_exists(install_dir):
252 |             if custom_dir:
253 |                 buildops.mkdir(install_dir)
254 |                 buildops.file_copytree(custom_dir, install_dir)
255 |             else:
256 |                 buildops.cmd(
257 |                     ["git", "clone", "--branch", clone_branch, clone_url],
258 |                     cwd=self.buildozer.platform_dir,
259 |                     env=self.buildozer.environ)
260 |         elif self.platform_update:
261 |             if custom_dir:
262 |                 buildops.file_copytree(custom_dir, install_dir)
263 |             else:
264 |                 buildops.cmd(
265 |                     ["git", "clean", "-dxf"],
266 |                     cwd=install_dir,
267 |                     env=self.buildozer.environ)
268 |                 buildops.cmd(
269 |                     ["git", "pull", "origin", clone_branch],
270 |                     cwd=install_dir,
271 |                     env=self.buildozer.environ)
272 |         return install_dir
273 | 


--------------------------------------------------------------------------------
/buildozer/targets/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kivy/buildozer/abc2d7e66c8abe096a95ed58befe6617f7efdad0/buildozer/targets/__init__.py


--------------------------------------------------------------------------------
/buildozer/targets/osx.py:
--------------------------------------------------------------------------------
  1 | """
  2 | OSX target, based on kivy-sdk-packager
  3 | """
  4 | 
  5 | import sys
  6 | if sys.platform != 'darwin':
  7 |     raise NotImplementedError('This will only work on osx')
  8 | 
  9 | from os.path import exists, join, abspath, dirname
 10 | from subprocess import check_call, check_output
 11 | import urllib.error
 12 | 
 13 | import buildozer.buildops as buildops
 14 | from buildozer.target import Target
 15 | 
 16 | 
 17 | class TargetOSX(Target):
 18 | 
 19 |     targetname = "osx"
 20 | 
 21 |     def ensure_sdk(self):
 22 |         self.logger.info('Check if kivy-sdk-packager exists')
 23 |         if exists(
 24 |                 join(self.buildozer.platform_dir, 'kivy-sdk-packager-master')):
 25 |             self.logger.info(
 26 |                     'kivy-sdk-packager found at '
 27 |                     '{}'.format(self.buildozer.platform_dir))
 28 |             return
 29 | 
 30 |         self.logger.info('kivy-sdk-packager does not exist, clone it')
 31 |         platdir = self.buildozer.platform_dir
 32 |         buildops.download(
 33 |             'https://github.com/kivy/kivy-sdk-packager/archive/',
 34 |             'master.zip',
 35 |             cwd=platdir)
 36 |         buildops.file_extract(
 37 |             'master.zip', cwd=platdir, env=self.buildozer.environ)
 38 |         buildops.file_remove(join(platdir, 'master.zip'))
 39 | 
 40 |     def download_kivy(self, cwd):
 41 |         current_kivy_vers = self.buildozer.config.get('app', 'osx.kivy_version')
 42 | 
 43 |         if exists('/Applications/Kivy.app'):
 44 |             self.logger.info('Kivy found in Applications dir...')
 45 |             buildops.file_copy('/Applications/Kivy.app', 'Kivy.app', cwd=cwd)
 46 |         else:
 47 |             if not exists(join(cwd, 'Kivy.dmg')):
 48 |                 self.logger.info('Downloading kivy...')
 49 |                 try:
 50 |                     buildops.download(
 51 |                         f'https://kivy.org/downloads/{current_kivy_vers}/',
 52 |                         'Kivy.dmg',
 53 |                         cwd=cwd
 54 |                     )
 55 |                 except urllib.error.URLError:
 56 |                     self.logger.error(
 57 |                         "Unable to download the Kivy App. "
 58 |                         "Check osx.kivy_version in your buildozer.spec, and "
 59 |                         "verify Kivy servers are accessible. "
 60 |                         "https://kivy.org/downloads/")
 61 |                     buildops.file_remove(join(cwd, "Kivy.dmg"))
 62 |                     sys.exit(1)
 63 | 
 64 |             self.logger.info('Extracting and installing Kivy...')
 65 |             check_call(('hdiutil', 'attach', cwd + '/Kivy.dmg'))
 66 |             buildops.file_copytree(
 67 |                 '/Volumes/Kivy/Kivy.app', cwd + '/Kivy.app'
 68 |             )
 69 | 
 70 |     def ensure_kivyapp(self):
 71 |         self.logger.info('check if Kivy.app exists in local dir')
 72 |         kivy_app_dir = join(self.buildozer.platform_dir, 'kivy-sdk-packager-master', 'osx')
 73 | 
 74 |         if exists(join(kivy_app_dir, 'Kivy.app')):
 75 |             self.logger.info('Kivy.app found at ' + kivy_app_dir)
 76 |         else:
 77 |             self.download_kivy(kivy_app_dir)
 78 | 
 79 |     def check_requirements(self):
 80 |         self.ensure_sdk()
 81 |         self.ensure_kivyapp()
 82 | 
 83 |     def build_package(self):
 84 |         self.logger.info('Building package')
 85 | 
 86 |         bc = self.buildozer.config
 87 |         bcg = bc.get
 88 |         package_name = bcg('app', 'package.name')
 89 |         domain = bcg('app', 'package.domain')
 90 |         title = bcg('app', 'title')
 91 |         app_deps = open('requirements.txt').read()
 92 |         icon = bc.getdefault('app', 'icon.filename', '')
 93 |         version = self.buildozer.get_version()
 94 |         author = bc.getdefault('app', 'author', '')
 95 | 
 96 |         self.logger.info('Create {}.app'.format(package_name))
 97 |         cwd = join(self.buildozer.platform_dir, 'kivy-sdk-packager-master', 'osx')
 98 |         # remove kivy from app_deps
 99 |         app_deps = [a for a in app_deps.split('\n') if not a.startswith('#') and a not in ['kivy', '']]
100 | 
101 |         cmd = [
102 |             'Kivy.app/Contents/Resources/script',
103 |             '-m', 'pip', 'install',
104 |              ]
105 |         cmd.extend(app_deps)
106 |         check_output(cmd, cwd=cwd)
107 | 
108 |         cmd = [
109 |             sys.executable,
110 |             'package_app.py',
111 |             self.buildozer.app_dir,
112 |             '--appname={}'.format(package_name),
113 |             '--bundlename={}'.format(title),
114 |             '--bundleid={}'.format(domain),
115 |             '--bundleversion={}'.format(version),
116 |             '--displayname={}'.format(title)
117 |               ]
118 |         if icon:
119 |             cmd.append('--icon={}'.format(icon))
120 |         if author:
121 |             cmd.append('--author={}'.format(author))
122 | 
123 |         check_output(cmd, cwd=cwd)
124 | 
125 |         self.logger.info('{}.app created.'.format(package_name))
126 |         self.logger.info('Creating {}.dmg'.format(package_name))
127 |         check_output(
128 |             ('sh', '-x', 'create-osx-dmg.sh', package_name + '.app', package_name, '-s', '1'),
129 |             cwd=cwd)
130 |         self.logger.info('{}.dmg created'.format(package_name))
131 |         self.logger.info('moving {}.dmg to bin.'.format(package_name))
132 | 
133 |         package_name = package_name + '.dmg'
134 |         binpath = join(
135 |             self.buildozer.user_build_dir or
136 |             dirname(abspath(self.buildozer.specfilename)), 'bin') + '/' + package_name
137 |         buildops.file_copy(
138 |             join(cwd, package_name),
139 |             binpath)
140 |         self.logger.info('All Done!')
141 | 
142 |     def install_platform(self):
143 |         # ultimate configuration check.
144 |         # some of our configuration cannot be checked without platform.
145 |         self.check_configuration_tokens()
146 |         #
147 |         self.buildozer.environ.update({
148 |             'PACKAGES_PATH': self.buildozer.global_packages_dir,
149 |         })
150 | 
151 |     def get_available_packages(self):
152 |         return ['kivy', 'python3']
153 | 
154 |     def run_commands(self, args):
155 |         if not args:
156 |             self.logger.error('Missing target command')
157 |             self.buildozer.usage()
158 |             sys.exit(1)
159 | 
160 |         result = []
161 |         last_command = []
162 |         for arg in args:
163 |             if not arg.startswith('--'):
164 |                 if last_command:
165 |                     result.append(last_command)
166 |                     last_command = []
167 |                 last_command.append(arg)
168 |             else:
169 |                 if not last_command:
170 |                     self.logger.error('Argument passed without a command')
171 |                     self.buildozer.usage()
172 |                     sys.exit(1)
173 |                 last_command.append(arg)
174 |         if last_command:
175 |             result.append(last_command)
176 | 
177 |         config_check = False
178 | 
179 |         for item in result:
180 |             command, args = item[0], item[1:]
181 |             if not hasattr(self, 'cmd_{0}'.format(command)):
182 |                 self.logger.error('Unknown command {0}'.format(command))
183 |                 sys.exit(1)
184 | 
185 |             func = getattr(self, 'cmd_{0}'.format(command))
186 | 
187 |             need_config_check = not hasattr(func, '__no_config')
188 |             if need_config_check and not config_check:
189 |                 config_check = True
190 |                 self.check_configuration_tokens()
191 | 
192 |             func(args)
193 | 
194 | 
195 | def get_target(buildozer):
196 |     return TargetOSX(buildozer)
197 | 


--------------------------------------------------------------------------------
/buildozer/tools/packer/.gitignore:
--------------------------------------------------------------------------------
1 | packer_cache
2 | output-kivy-buildozer-vm
3 | 


--------------------------------------------------------------------------------
/buildozer/tools/packer/CHANGELOG:
--------------------------------------------------------------------------------
1 | ## Release 2.0 - 13 May 2017
2 | 
3 | - Brand new VM image using latest zesty (x)ubuntu (64 bits)
4 | - Image created for Virtualbox 5.1.22, with guest-tools
5 | - Increase disk space to 20GB
6 | - /build can store buildozer builds (specify with build_dir=/build/myapp)
7 | - Rewrite welcome document
8 | 


--------------------------------------------------------------------------------
/buildozer/tools/packer/Makefile:
--------------------------------------------------------------------------------
 1 | .PHONY: all build
 2 | VERSION := 2.0
 3 | ANN1 = udp://public.popcorn-tracker.org:6969/announce
 4 | ANN2 = udp://ipv4.tracker.harry.lu/announce
 5 | ANN3 = udp://tracker.opentrackr.org:1337/announce
 6 | ANN4 = udp://9.rarbg.com:2710/announce
 7 | ANN5 = udp://explodie.org:6969
 8 | ANN6 = udp://tracker.coppersurfer.tk:6969
 9 | ANN7 = udp://tracker.leechers-paradise.org:6969
10 | ANN8 = udp://zer0day.ch:1337
11 | TORRENT_ANNOUNCE := ${ANN1},${ANN2},${ANN3},${ANN4},${ANN5},${ANN6},${ANN7},${ANN8}
12 | PACKAGE_FILENAME = kivy-buildozer-vm-${VERSION}.zip
13 | 
14 | all: packer repackage torrent upload
15 | 
16 | build:
17 | 	packer-io build template.json
18 | 
19 | repackage:
20 | 	cd output-kivy-buildozer-vm && mv Kivy kivy-buildozer-vm-${VERSION}
21 | 	cd output-kivy-buildozer-vm && zip -0 -r ${PACKAGE_FILENAME} kivy-buildozer-vm-${VERSION}
22 | 
23 | torrent:
24 | 	rm -f output-kivy-buildozer-vm/kivy-buildozer-vm.torrent
25 | 	mktorrent \
26 | 		-a ${TORRENT_ANNOUNCE} \
27 | 		-o output-kivy-buildozer-vm/kivy-buildozer-vm.torrent \
28 | 		-w https://txzone.net/files/torrents/${PACKAGE_FILENAME} \
29 | 		-v output-kivy-buildozer-vm/${PACKAGE_FILENAME}
30 | 
31 | upload:
32 | 	# txzone only for now, don't have access to kivy server
33 | 	rsync -avz --info=progress2 output-kivy-buildozer-vm/${PACKAGE_FILENAME} txzone.net:/var/www/websites/txzone.net/files/torrents/
34 | 	rsync -avz --info=progress2 output-kivy-buildozer-vm/kivy-buildozer-vm.torrent txzone.net:/var/www/websites/txzone.net/files/torrents/
35 | 


--------------------------------------------------------------------------------
/buildozer/tools/packer/README.md:
--------------------------------------------------------------------------------
 1 | # Introduction
 2 | 
 3 | This is the packer template for building the official Kivy/Buildozer VM.
 4 | It is based on xubuntu.
 5 | 
 6 | # Configure
 7 | 
 8 | You want to edit `http/preseed.cfg` and `template.json` before building an image.
 9 | 
10 | # Build
11 | 
12 | ```
13 | make packer
14 | ```
15 | 
16 | # Release
17 | 
18 | 1. Update Makefile to increase the version number
19 | 2. Update the CHANGELOG
20 | 3. Commit
21 | 
22 | Then:
23 | 
24 | ```
25 | make all
26 | # make packer       < build the image
27 | # make repackage    < just zip it (no compression)
28 | # make torrent      < create the torrent
29 | # make upload       < upload on txzone.net (tito only)
30 | ```
31 | 
32 | 
33 | # Notes
34 | 
35 | - trigger a build on travis, torrent creation and gdrive upload when buildozer is
36 |   released
37 | - https://www.packer.io/docs/builders/virtualbox-ovf.html
38 | 


--------------------------------------------------------------------------------
/buildozer/tools/packer/http/buildozer.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Version=1.0
3 | Type=Application
4 | Name=Buildozer
5 | Exec=firefox -url "file:///usr/share/applications/buildozer-welcome/index.html" -width 600 -height 800
6 | Icon=/usr/share/applications/buildozer-welcome/icon.png
7 | Categories=Application;Development;favourite;
8 | 


--------------------------------------------------------------------------------
/buildozer/tools/packer/http/kivy-icon-96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kivy/buildozer/abc2d7e66c8abe096a95ed58befe6617f7efdad0/buildozer/tools/packer/http/kivy-icon-96.png


--------------------------------------------------------------------------------
/buildozer/tools/packer/http/preseed.cfg:
--------------------------------------------------------------------------------
  1 | ### Options to set on the command line
  2 | d-i debian-installer/locale string en_US.UTF-8
  3 | d-i console-setup/ask_detect boolean false
  4 | d-i keyboard-configuration/layoutcode string us
  5 | d-i keyboard-configuration/variant string us
  6 | 
  7 | ### Network
  8 | d-i netcfg/choose_interface select auto
  9 | d-i netcfg/get_hostname string kivy-buildozer-vm
 10 | d-i netcfg/get_domain string local
 11 | d-i netcfg/wireless_wep string
 12 | 
 13 | ### Mirror
 14 | d-i mirror/country string manual
 15 | d-i mirror/http/hostname string fr.archive.ubuntu.com
 16 | d-i mirror/http/directory string /ubuntu
 17 | # d-i mirror/http/hostname string 192.168.1.16:3142
 18 | # d-i mirror/http/directory string /fr.archive.ubuntu.com/ubuntu
 19 | d-i mirror/http/proxy string
 20 | 
 21 | ### Time
 22 | d-i time/zone string UTC
 23 | d-i clock-setup/utc boolean true
 24 | 
 25 | ### Partitioning
 26 | d-i partman-auto/method string lvm
 27 | d-i partman-lvm/device_remove_lvm boolean true
 28 | d-i partman-md/device_remove_md boolean true
 29 | 
 30 | # Write the changes to disks and configure LVM?
 31 | d-i partman-lvm/confirm boolean true
 32 | d-i partman-lvm/confirm_nooverwrite boolean true
 33 | 
 34 | d-i partman-auto-lvm/guided_size string max
 35 | d-i partman-auto/choose_recipe select atomic
 36 | d-i partman/default_filesystem string ext4
 37 | 
 38 | # This makes partman automatically partition without confirmation, provided
 39 | # that you told it what to do using one of the methods above.
 40 | #d-i partman-partitioning/confirm_write_new_label boolean true
 41 | d-i partman/confirm_write_new_label boolean true
 42 | d-i partman/choose_partition select finish
 43 | d-i partman/confirm boolean true
 44 | d-i partman/confirm_nooverwrite boolean true
 45 | 
 46 | ### User Account
 47 | 
 48 | # root account
 49 | #d-i passwd/root-login boolean true
 50 | #d-i passwd/make-user boolean false
 51 | #d-i passwd/root-password password abcdabcd
 52 | #d-i passwd/root-password-again password abcdabcd
 53 | 
 54 | # a user account
 55 | d-i passwd/user-fullname string kivy
 56 | d-i passwd/username string kivy
 57 | d-i passwd/user-password password kivy
 58 | d-i passwd/user-password-again password kivy
 59 | 
 60 | # you might want to configure auto login.
 61 | #d-i passwd/auto-login boolean true
 62 | 
 63 | d-i user-setup/allow-password-weak boolean true
 64 | d-i user-setup/encrypt-home boolean false
 65 | 
 66 | # You can choose to install restricted and universe software, or to install
 67 | # software from the backports repository.
 68 | d-i apt-setup/restricted boolean true
 69 | d-i apt-setup/universe boolean true
 70 | # d-i apt-setup/backports boolean true
 71 | 
 72 | # Uncomment this if you don't want to use a network mirror.
 73 | # d-i apt-setup/use_mirror boolean false
 74 | 
 75 | # Select which update services to use; define the mirrors to be used.
 76 | # Values shown below are the normal defaults.
 77 | d-i apt-setup/services-select multiselect security
 78 | d-i apt-setup/security_host string security.ubuntu.com
 79 | d-i apt-setup/security_path string /ubuntu
 80 | 
 81 | ### Packages
 82 | tasksel tasksel/first multiselect xubuntu-desktop
 83 | 
 84 | d-i pkgsel/include string openssh-server build-essential ibus-hangul
 85 | d-i pkgsel/upgrade select none
 86 | d-i pkgsel/update-policy select none
 87 | d-i pkgsel/language-packs multiselect en, ko
 88 | # required by ubuntu server iso, but not required by netboot iso
 89 | d-i pkgsel/install-language-support boolean true
 90 | 
 91 | popularity-contest popularity-contest/participate boolean false
 92 | 
 93 | d-i lilo-installer/skip boolean true
 94 | 
 95 | ### Bootloader
 96 | d-i grub-installer/only_debian boolean true
 97 | d-i grub-installer/with_other_os boolean true
 98 | 
 99 | ### Finishing
100 | d-i finish-install/reboot_in_progress note
101 | 


--------------------------------------------------------------------------------
/buildozer/tools/packer/http/wallpaper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kivy/buildozer/abc2d7e66c8abe096a95ed58befe6617f7efdad0/buildozer/tools/packer/http/wallpaper.png


--------------------------------------------------------------------------------
/buildozer/tools/packer/http/welcome/buildozer.css:
--------------------------------------------------------------------------------
 1 | body {
 2 |     padding: 20px;
 3 | }
 4 | 
 5 | pre {
 6 |     padding: 20px;
 7 | }
 8 | 
 9 | .warning {
10 |     background-color: #fff5f6;
11 |     border-left: 2px solid #c0392b;
12 |     padding: 20px;
13 |     margin-bottom: 2.5rem;
14 |     padding-bottom: 1px;
15 | }
16 | 


--------------------------------------------------------------------------------
/buildozer/tools/packer/http/welcome/index.html:
--------------------------------------------------------------------------------
  1 | 
  2 | 
  3 |     
  4 |     Kivy/Buildozer VM
  5 |     
  6 |     
  8 | 
  9 | 
 10 | 

Welcome to the Kivy/Buildozer VM

11 | 15 | 16 |

17 | Thanks for using Kivy/Buildozer VM. It has been installed only for 18 | packaging Kivy application for Android.
19 | Credentials: username: kivy / password: kivy 20 |

21 | 22 |

How to use the VM

23 |

24 | Buildozer is ready to be used. You'll need internet connection for 25 | download the Android SDK/NDK (automatically done), and during the first 26 | compilation. 27 |
28 | It is preferable to add a share a folder 29 | between your host and the VM, then build from there.
30 | 31 | By the time we shipped the VM and you using it, you may need to 32 | update buildozer. 33 |

34 |

35 | Don't try to use latest Android SDK or NDK. The defaults from buildozer 36 | works: Android SDK 20, Android NDK 9c. Recent Android SDK doesn't work 37 | the same as before (no more android command), and python-for-android 38 | project have issues with it. As for NDK, you can use 13b, it works too. 39 |

40 |
    41 |
  1. First time only, in your project directory: buildozer init
  2. 42 |
  3. Adjust the buildozer.spec: 43 |
    [buildozer]
     44 | # change the name of your app
     45 | package.name = myapp
     46 | 
     47 | # change the domain of your package
     48 | package.domain = com.mydomain
     49 | 
     50 | # specify hostpython2 manually. If you want to use python 3, check buildozer
     51 | # README about it, the VM is not preinstalled for that.
     52 | requirements = hostpython2,kivy
     53 | 
     54 | [buildozer]
     55 | # update the build directory (issue with virtualbox shared folder and symlink)
     56 | build_dir = /build/myapp
     57 | 
  4. 58 |
  5. Build your application: buildozer android debug
  6. 59 |
  7. Build and deploy, run and get the logs: buildozer android debug deploy run logcat
  8. 60 |
61 | 62 |

Share a folder

63 |

64 | Virtualbox allows you to share a folder between your computer and the 65 | VM. To do, just: 66 |

    67 |
  1. Go into Devices > Shared Folders > Shared Folders Settings
  2. 68 |
  3. Add a new folder, select the automount option
  4. 69 |
  5. Reboot the VM (that's easier)
  6. 70 |
  7. You'll find your new directory at /media/sf_directoryname
  8. 71 |
72 |

73 | 74 |
75 | Virtualbox doesn't support symlink in Shared Folder anymore. So buildozer 76 | will fail during the build.
77 | We already created a /build directory where you can put your 78 | build in it. Edit your buildozer.spec: 79 |
[buildozer]
 80 | build_dir = /build/buildozer-myapp
81 |
82 | 83 |

Update buildozer

84 |

85 | The buildozer version you have may be outdated, as well as the dependencies. 86 | The best is to regularly update buildozer: 87 |

sudo pip install -U buildozer
88 |

89 | 90 |

Cleaning cache

91 | 92 |

93 | The simplest way to update kivy and other modules is to clean all the 94 | buildozer cache, and rebuild everything. 95 | 96 |

rm -rf ~/.buildozer/android/packages
97 |

98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /buildozer/tools/packer/http/welcome/milligram.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Milligram v1.3.0 3 | * https://milligram.github.io 4 | * 5 | * Copyright (c) 2017 CJ Patoilo 6 | * Licensed under the MIT license 7 | */ 8 | 9 | *,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#9b4dca;border:0.1rem solid #9b4dca;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#9b4dca;border-color:#9b4dca}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#9b4dca}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#9b4dca}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#9b4dca}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#9b4dca}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #9b4dca;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#9b4dca;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.container{margin:0 auto;max-width:112.0rem;padding:0 2.0rem;position:relative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{-ms-grid-row-align:center;align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#9b4dca;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right} 10 | 11 | /*# sourceMappingURL=milligram.min.css.map */ -------------------------------------------------------------------------------- /buildozer/tools/packer/launch: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /usr/bin/qemu-system-x86_64 -redir tcp:3213::22 -vga qxl -display sdl -netdev user,id=user.0 -device virtio-net,netdev=user.0 -drive file=output-from-netboot-iso/ubuntu.qcow2,if=virtio -boot once=d -name sanitytest -machine type=pc-1.0,accel=kvm -m 512M -vnc 0.0.0.0:47 4 | -------------------------------------------------------------------------------- /buildozer/tools/packer/scripts/additional-packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | # Don't use openjdk-9, the conf directory is missing, and we get 3 | # an error when using the android sdk: 4 | # "Can't read cryptographic policy directory: unlimited" 5 | 6 | wget https://bootstrap.pypa.io/get-pip.py 7 | python get-pip.py 8 | rm get-pip.py 9 | 10 | apt-get -y install lib32stdc++6 lib32z1 lib32ncurses5 11 | apt-get -y install build-essential 12 | apt-get -y install git openjdk-8-jdk --no-install-recommends zlib1g-dev 13 | pip install cython buildozer python-for-android 14 | -------------------------------------------------------------------------------- /buildozer/tools/packer/scripts/install-virtualbox-guest-additions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Mount the disk image 4 | cd /tmp 5 | mkdir /tmp/isomount 6 | mount -t iso9660 /dev/sr1 /tmp/isomount 7 | 8 | # Install the drivers 9 | /tmp/isomount/VBoxLinuxAdditions.run 10 | 11 | # Cleanup 12 | umount isomount 13 | -------------------------------------------------------------------------------- /buildozer/tools/packer/scripts/minimize.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Remove unwanted applications 4 | apt-get -y remove --purge libreoffice* 5 | apt-get -y remove --purge pidgin* 6 | apt-get -y remove --purge thunderbird* 7 | apt-get -y remove --purge fonts-noto-cjk 8 | 9 | # Remove APT cache 10 | apt-get -y --purge autoremove 11 | apt-get -y clean 12 | 13 | # Cleanup log files 14 | find /var/log -type f | while read f; do echo -ne '' > $f; done; 15 | 16 | # Whiteout root 17 | count=`df --sync -kP / | tail -n1 | awk -F ' ' '{print $4}'`; 18 | count=$(expr $count - 1) 19 | dd if=/dev/zero of=/tmp/whitespace bs=1024 count=$count; 20 | rm /tmp/whitespace; 21 | 22 | # Whiteout /boot 23 | count=`df --sync -kP /boot | tail -n1 | awk -F ' ' '{print $4}'`; 24 | count=$(expr $count - 1) 25 | dd if=/dev/zero of=/boot/whitespace bs=1024 count=$count; 26 | rm /boot/whitespace; 27 | 28 | swappart=`cat /proc/swaps | tail -n1 | awk -F ' ' '{print $1}'` 29 | swapoff $swappart; 30 | dd if=/dev/zero of=$swappart; 31 | mkswap $swappart; 32 | swapon $swappart; 33 | 34 | # Zero free space to aid VM compression 35 | dd if=/dev/zero of=/EMPTY bs=1M 36 | rm -f /EMPTY 37 | 38 | # Remove bash history 39 | unset HISTFILE 40 | rm -f /root/.bash_history 41 | -------------------------------------------------------------------------------- /buildozer/tools/packer/scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # xfconf doesn't work with sudo, even with XAUTHORITY + DISPLAY 3 | # seems that the user need to log to be able to use them. 4 | 5 | # keep them for reference for now. 6 | # change theme (works better for this wallpaper) 7 | # xfconf-query -c xfce4-desktop \ 8 | # --property /backdrop/screen0/monitor0/workspace0/last-image \ 9 | # --set /usr/share/backgrounds/kivy-wallpaper.png 10 | # xfconf-query -c xsettings \ 11 | # --property /Net/ThemeName \ 12 | # --set Adwaita 13 | # xfconf-query -c xsettings \ 14 | # --property /Net/IconThemeName \ 15 | # --set elementary-xfce-darker 16 | 17 | 18 | 19 | set -x 20 | 21 | # ensure the kivy user can mount shared folders 22 | adduser kivy vboxsf 23 | 24 | # create a space specifically for builds 25 | mkdir /build 26 | chown kivy /build 27 | 28 | # add a little face 29 | wget $PACKER_HTTP_ADDR/kivy-icon-96.png 30 | mv kivy-icon-96.png /home/kivy/.face 31 | chown kivy.kivy /home/kivy/.face 32 | 33 | # set wallpaper 34 | wget $PACKER_HTTP_ADDR/wallpaper.png 35 | mv wallpaper.png /usr/share/backgrounds/kivy-wallpaper.png 36 | sed -i "s:/usr/share/xfce4/backdrops/xubuntu-wallpaper.png:/usr/share/backgrounds/kivy-wallpaper.png:g" /etc/xdg/xdg-xubuntu/xfce4/xfconf/xfce-perchannel-xml/xfce4-desktop.xml 37 | sed -i "s:Greybird:Adwaita:g" /etc/xdg/xfce4/xfconf/xfce-perchannel-xml/xsettings.xml 38 | sed -i "s:Greybird:Adwaita:g" /etc/xdg/xdg-xubuntu/xfce4/xfconf/xfce-perchannel-xml/xsettings.xml 39 | sed -i "s:Greybird:Adwaita:g" /etc/xdg/xdg-xubuntu/xfce4/xfconf/xfce-perchannel-xml/xfwm4.xml 40 | sed -i "s:Greybird:Adwaita:g" /etc/xdg/xdg-xubuntu/xfce4/xfconf/xfce-perchannel-xml/xfce4-notifyd.xml 41 | sed -i "s:elementary-xfce-darker:elementary-xfce-darkest:g" /etc/xdg/xdg-xubuntu/xfce4/xfconf/xfce-perchannel-xml/xsettings.xml 42 | sed -i "s:elementary-xfce-dark:elementary-xfce-darkest:g" /etc/xdg/xfce4/xfconf/xfce-perchannel-xml/xsettings.xml 43 | 44 | # add desktop icon 45 | wget $PACKER_HTTP_ADDR/buildozer.desktop 46 | mkdir -p /home/kivy/Desktop 47 | cp buildozer.desktop /home/kivy/Desktop/ 48 | chown kivy.kivy -R /home/kivy/Desktop 49 | chmod +x /home/kivy/Desktop/buildozer.desktop 50 | mv buildozer.desktop /usr/share/applications/ 51 | sed -i "s:^favorites=.*$:favorites=buildozer.desktop,exo-terminal-emulator.desktop,exo-web-browser.desktop,xfce-keyboard-settings.desktop,exo-file-manager.desktop,org.gnome.Software.desktop,xfhelp4.desktop:g" /etc/xdg/xdg-xubuntu/xfce4/whiskermenu/defaults.rc 52 | 53 | # copy welcome directory 54 | mkdir -p /usr/share/applications/buildozer-welcome 55 | cd /usr/share/applications/buildozer-welcome 56 | wget $PACKER_HTTP_ADDR/welcome/milligram.min.css 57 | wget $PACKER_HTTP_ADDR/welcome/buildozer.css 58 | wget $PACKER_HTTP_ADDR/welcome/index.html 59 | wget $PACKER_HTTP_ADDR/kivy-icon-96.png 60 | mv kivy-icon-96.png icon.png 61 | cd - 62 | -------------------------------------------------------------------------------- /buildozer/tools/packer/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": { 3 | "disk_size": "20480", 4 | "disk_format": "ovf", 5 | "ssh_username": "kivy", 6 | "ssh_password": "kivy", 7 | "hostname": "kivyvm" 8 | }, 9 | "description": "Build a Xubuntu Virtual Machine", 10 | "builders": [{ 11 | "type": "virtualbox-iso", 12 | "name": "kivy-buildozer-vm", 13 | "http_directory": "http", 14 | "iso_checksum": "6131e2cc90cf30407af18f3f1af16c54bf58ffc8", 15 | "iso_checksum_type": "sha1", 16 | "iso_url": "http://archive.ubuntu.com/ubuntu/dists/zesty/main/installer-amd64/current/images/netboot/mini.iso", 17 | "ssh_username": "{{user `ssh_username`}}", 18 | "ssh_password": "{{user `ssh_password`}}", 19 | "boot_wait": "3s", 20 | "boot_command": [ 21 | "", 22 | "/linux noapic preseed/url=http://{{.HTTPIP}}:{{.HTTPPort}}/preseed.cfg ", 23 | "hostname={{user `hostname`}} ", 24 | "debian-installer=en_US auto locale=en_US kbd-chooser/method=us ", 25 | "fb=false ", 26 | "keyboard-configuration/modelcode=SKIP keyboard-configuration/layout=USA ", 27 | "keyboard-configuration/variant=USA console-setup/ask_detect=false ", 28 | "initrd=/initrd.gz -- " 29 | ], 30 | "disk_size": "{{user `disk_size`}}", 31 | "format": "{{user `disk_format`}}", 32 | "headless": false, 33 | "shutdown_command": "echo {{user `ssh_password`}} | sudo -S shutdown -P now", 34 | "vm_name": "Kivy/Buildozer VM", 35 | "guest_os_type": "Ubuntu_64", 36 | "guest_additions_mode": "attach", 37 | "ssh_wait_timeout": "120m" 38 | }], 39 | "provisioners": [{ 40 | "type": "shell", 41 | "execute_command": "echo {{user `ssh_password`}} | {{ .Vars }} sudo -E -S sh '{{ .Path }}'", 42 | "scripts": [ 43 | "scripts/install-virtualbox-guest-additions.sh", 44 | "scripts/setup.sh", 45 | "scripts/additional-packages.sh", 46 | "scripts/minimize.sh" 47 | ] 48 | }], 49 | "post-processors": [] 50 | } 51 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://www.sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Buildozer.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Buildozer.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Buildozer" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Buildozer" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.https://www.sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Buildozer.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Buildozer.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Buildozer documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Apr 20 16:56:31 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import os 15 | import re 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | #sys.path.insert(0, os.path.abspath('.')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autosectionlabel'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'Buildozer' 45 | copyright = u'2014-2023, Kivy Team and other contributors' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | 52 | # Lookup the version from the Buildozer module, without installing it 53 | # since readthedocs.org may have issue to install it. 54 | # Read the version from the __init__.py file, without importing it. 55 | def get_version(): 56 | with open( 57 | os.path.join(os.path.abspath("../.."), "buildozer", "__init__.py") 58 | ) as fp: 59 | for line in fp: 60 | m = re.search(r'^\s*__version__\s*=\s*([\'"])([^\'"]+)\1\s*$', line) 61 | if m: 62 | return m.group(2) 63 | 64 | 65 | # The short X.Y version. 66 | version = get_version() 67 | # The full version, including alpha/beta/rc tags. 68 | release = get_version() 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | #language = None 73 | 74 | # There are two options for replacing |today|: either, you set today to some 75 | # non-false value, then it is used: 76 | #today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | #today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | exclude_patterns = [] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all documents. 85 | #default_role = None 86 | 87 | # If true, '()' will be appended to :func: etc. cross-reference text. 88 | #add_function_parentheses = True 89 | 90 | # If true, the current module name will be prepended to all description 91 | # unit titles (such as .. function::). 92 | #add_module_names = True 93 | 94 | # If true, sectionauthor and moduleauthor directives will be shown in the 95 | # output. They are ignored by default. 96 | #show_authors = False 97 | 98 | # The name of the Pygments (syntax highlighting) style to use. 99 | pygments_style = 'sphinx' 100 | 101 | # A list of ignored prefixes for module index sorting. 102 | #modindex_common_prefix = [] 103 | 104 | # If true, keep warnings as "system message" paragraphs in the built documents. 105 | #keep_warnings = False 106 | 107 | 108 | # -- Options for HTML output --------------------------------------------------- 109 | 110 | # The theme to use for HTML and HTML Help pages. See the documentation for 111 | # a list of builtin themes. 112 | html_theme = 'default' 113 | 114 | # Theme options are theme-specific and customize the look and feel of a theme 115 | # further. For a list of options available for each theme, see the 116 | # documentation. 117 | #html_theme_options = {} 118 | 119 | # Add any paths that contain custom themes here, relative to this directory. 120 | #html_theme_path = [] 121 | 122 | # The name for this set of Sphinx documents. If None, it defaults to 123 | # " v documentation". 124 | #html_title = None 125 | 126 | # A shorter title for the navigation bar. Default is the same as html_title. 127 | #html_short_title = None 128 | 129 | # The name of an image file (relative to this directory) to place at the top 130 | # of the sidebar. 131 | #html_logo = None 132 | 133 | # The name of an image file (within the static path) to use as favicon of the 134 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 135 | # pixels large. 136 | #html_favicon = None 137 | 138 | # Add any paths that contain custom static files (such as style sheets) here, 139 | # relative to this directory. They are copied after the builtin static files, 140 | # so a file named "default.css" will overwrite the builtin "default.css". 141 | # html_static_path = ['_static'] 142 | 143 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 144 | # using the given strftime format. 145 | #html_last_updated_fmt = '%b %d, %Y' 146 | 147 | # If true, SmartyPants will be used to convert quotes and dashes to 148 | # typographically correct entities. 149 | #html_use_smartypants = True 150 | 151 | # Custom sidebar templates, maps document names to template names. 152 | #html_sidebars = {} 153 | 154 | # Additional templates that should be rendered to pages, maps page names to 155 | # template names. 156 | #html_additional_pages = {} 157 | 158 | # If false, no module index is generated. 159 | #html_domain_indices = True 160 | 161 | # If false, no index is generated. 162 | #html_use_index = True 163 | 164 | # If true, the index is split into individual pages for each letter. 165 | #html_split_index = False 166 | 167 | # If true, links to the reST sources are added to the pages. 168 | #html_show_sourcelink = True 169 | 170 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 171 | #html_show_sphinx = True 172 | 173 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 174 | #html_show_copyright = True 175 | 176 | # If true, an OpenSearch description file will be output, and all pages will 177 | # contain a tag referring to it. The value of this option must be the 178 | # base URL from which the finished HTML is served. 179 | #html_use_opensearch = '' 180 | 181 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 182 | #html_file_suffix = None 183 | 184 | # Output file base name for HTML help builder. 185 | htmlhelp_basename = 'Buildozerdoc' 186 | 187 | 188 | # -- Options for LaTeX output -------------------------------------------------- 189 | 190 | latex_elements = { 191 | # The paper size ('letterpaper' or 'a4paper'). 192 | #'papersize': 'letterpaper', 193 | 194 | # The font size ('10pt', '11pt' or '12pt'). 195 | #'pointsize': '10pt', 196 | 197 | # Additional stuff for the LaTeX preamble. 198 | #'preamble': '', 199 | } 200 | 201 | # Grouping the document tree into LaTeX files. List of tuples 202 | # (source start file, target name, title, author, documentclass [howto/manual]). 203 | latex_documents = [ 204 | ('index', 'Buildozer.tex', u'Buildozer Documentation', 205 | u'Kivy Team and other contributors', 'manual'), 206 | ] 207 | 208 | # The name of an image file (relative to this directory) to place at the top of 209 | # the title page. 210 | #latex_logo = None 211 | 212 | # For "manual" documents, if this is true, then toplevel headings are parts, 213 | # not chapters. 214 | #latex_use_parts = False 215 | 216 | # If true, show page references after internal links. 217 | #latex_show_pagerefs = False 218 | 219 | # If true, show URL addresses after external links. 220 | #latex_show_urls = False 221 | 222 | # Documents to append as an appendix to all manuals. 223 | #latex_appendices = [] 224 | 225 | # If false, no module index is generated. 226 | #latex_domain_indices = True 227 | 228 | 229 | # -- Options for manual page output -------------------------------------------- 230 | 231 | # One entry per manual page. List of tuples 232 | # (source start file, name, description, authors, manual section). 233 | man_pages = [ 234 | ('index', 'buildozer', 'Buildozer Documentation', 235 | ['Kivy Team and other contributors'], 1) 236 | ] 237 | 238 | # If true, show URL addresses after external links. 239 | #man_show_urls = False 240 | 241 | 242 | # -- Options for Texinfo output ------------------------------------------------ 243 | 244 | # Grouping the document tree into Texinfo files. List of tuples 245 | # (source start file, target name, title, author, 246 | # dir menu entry, description, category) 247 | texinfo_documents = [ 248 | ('index', 'Buildozer', u'Buildozer Documentation', 249 | 'Kivy Team and other contributors', 'Buildozer', 250 | 'Turns Python applications into binary packages ready for ' 251 | 'installation on a number of platforms.', 252 | 'Miscellaneous'), 253 | ] 254 | 255 | # Documents to append as an appendix to all manuals. 256 | #texinfo_appendices = [] 257 | 258 | # If false, no module index is generated. 259 | #texinfo_domain_indices = True 260 | 261 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 262 | #texinfo_show_urls = 'footnote' 263 | 264 | # If true, do not generate a @detailmenu in the "Top" node's menu. 265 | #texinfo_no_detailmenu = False 266 | -------------------------------------------------------------------------------- /docs/source/contact.rst: -------------------------------------------------------------------------------- 1 | .. _contact: 2 | 3 | Contact Us 4 | ========== 5 | 6 | If you are looking to contact the Kivy Team (who are responsible for managing the 7 | Buildozer project), including looking for support, please see our 8 | `latest contact details `_. -------------------------------------------------------------------------------- /docs/source/contribute.rst: -------------------------------------------------------------------------------- 1 | .. _contribute: 2 | 3 | Contribution Guidelines 4 | ======================= 5 | 6 | Buildozer is part of the `Kivy `_ ecosystem - a large group of 7 | products used by many thousands of developers for free, but it 8 | is built entirely by the contributions of volunteers. We welcome (and rely on) 9 | users who want to give back to the community by contributing to the project. 10 | 11 | Contributions can come in many forms. See the latest 12 | `Contribution Guidelines `_ 13 | for general guidelines of how you can help us. 14 | -------------------------------------------------------------------------------- /docs/source/faq.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | === 3 | 4 | Buildozer has an `online FAQ `_. It contains the answers to 5 | questions that repeatedly come up. 6 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Buildozer's documentation! 2 | ===================================== 3 | 4 | Buildozer is a development tool for turning Python 5 | applications into binary packages ready for installation on any of a number of 6 | platforms, including mobile devices. It automates the entire build process. 7 | 8 | The app developer provides a single "buildozer.spec" file, which describes the 9 | application's requirements and settings, such as title and icons. Buildozer can 10 | then create installable packages for Android, iOS, Windows, macOS and/or Linux. 11 | 12 | Buildozer has features to make 13 | building apps using the `Kivy framework `_ easier, 14 | but it can be used independently - even with other GUI frameworks. 15 | 16 | .. note:: 17 | python-for-android only runs on Linux or macOS. (On Windows, a Linux emulator is 18 | required.) 19 | 20 | Kivy for iOS only runs on macOS. 21 | 22 | Buildozer is managed by the `Kivy Team `_. It relies 23 | on its sibling projects: 24 | `python-for-android `_ for 25 | Android packaging, and 26 | `Kivy for iOS `_ for iOS packaging. 27 | 28 | Buildozer is released and distributed under the terms of the MIT license. You should have received a 29 | copy of the MIT license alongside your distribution. Our 30 | `latest license `_ 31 | is also available. 32 | 33 | 34 | .. note:: 35 | This tool is unrelated to the online build service, `buildozer.io`. 36 | 37 | .. toctree:: 38 | :maxdepth: 2 39 | 40 | installation 41 | quickstart 42 | specifications 43 | recipes 44 | faq 45 | contribute 46 | contact 47 | 48 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Buildozer is tested on Python 3.8 and above. 5 | Depending the platform you want to target, you might need more tools installed. 6 | Buildozer tries to give you hints or tries to install few things for 7 | you, but it doesn't cover every situation. 8 | 9 | First, install the buildozer project. 10 | 11 | The most-recently released version can be installed with:: 12 | 13 | pip install --user --upgrade buildozer 14 | 15 | Add the `--user` option if you are not using a virtual environment (not recommended). 16 | 17 | If you would like to install the latest version still under development:: 18 | 19 | pip install https://github.com/kivy/buildozer/archive/master.zip 20 | 21 | 22 | Targeting Android 23 | ----------------- 24 | 25 | Android on Ubuntu 20.04 and 22.04 (64bit) 26 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 27 | 28 | .. note:: 29 | Later versions of Ubuntu are expected to work. However only the latest 30 | `Long Term Support (LTS) release `_ 31 | is regularly tested. 32 | 33 | Additional installation required to support Android:: 34 | 35 | sudo apt update 36 | sudo apt install -y git zip unzip openjdk-17-jdk python3-pip autoconf libtool pkg-config zlib1g-dev libncurses5-dev libncursesw5-dev libtinfo5 cmake libffi-dev libssl-dev automake 37 | 38 | # add the following line at the end of your ~/.bashrc file 39 | export PATH=$PATH:~/.local/bin/ 40 | 41 | If `openjdk-17 `_ is not compatible with other installed programs, 42 | for Buildozer the minimum compatible openjdk version is 11. 43 | 44 | Then install the buildozer project with:: 45 | 46 | pip3 install --user --upgrade buildozer 47 | 48 | 49 | Android on Windows 10 or 11 50 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 51 | 52 | To use Buildozer on Windows, you need first to enable Windows Subsystem for Linux (WSL) and 53 | `install a Linux distribution `_. 54 | 55 | These instructions were tested with WSL 1 and Ubuntu 18.04 LTS, and WSL2 with Ubuntu 20.04 and 22.04. 56 | 57 | After installing WSL and Ubuntu on your Windows machine, open Ubuntu, run the commands listed in the previous section, 58 | and restart your WSL terminal to enable the path change. 59 | 60 | Copy your Kivy project directory from the Windows partition to the WSL partition. 61 | 62 | .. warning:: 63 | It is important to use the WSL partition. The Android SDK for Linux does not work on Windows' NTFS drives. 64 | This will lead to obscure failures. 65 | 66 | For debugging, WSL does not have direct access to USB. Copy the .apk file to the Windows partition and run ADB 67 | (Android Debug Bridge) from a Windows prompt. ADB is part of Android Studio, if you do not have this installed 68 | you can install just the platform tools which also contain ADB. 69 | 70 | - Visit the `Android SDK Platform Tools `_ page, and 71 | select "Download SDK Platform-Tools for Windows". 72 | 73 | - Unzip the downloaded file to a new folder. For example, `C:\\platform-tools\\` 74 | 75 | Before Using Buildozer 76 | ~~~~~~~~~~~~~~~~~~~~~~ 77 | 78 | If you wish, clone your code to a new folder where the build process will run. 79 | 80 | You don't need to create a virtualenv for your code requirements. But just add these requirements to a configuration 81 | file called `buildozer.spec` as you will see in the following sections. 82 | 83 | Before running Buildozer in your code folder, remember to go into the Buildozer folder and activate the Buildozer 84 | virtualenv. 85 | 86 | Android on macOS 87 | ~~~~~~~~~~~~~~~~ 88 | 89 | Additional installation required to support macOS:: 90 | 91 | python3 -m pip install --user --upgrade buildozer # the --user should be removed if you do this in a venv 92 | 93 | 94 | TroubleShooting 95 | ~~~~~~~~~~~~~~~ 96 | 97 | Buildozer stuck on "Installing/updating SDK platform tools if necessary" 98 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 99 | 100 | Press "y" then enter to continue, the license acceptance system is silently waiting for your input 101 | 102 | 103 | Aidl not found, please install it. 104 | """""""""""""""""""""""""""""""""" 105 | 106 | Buildozer didn't install a necessary package 107 | 108 | :: 109 | 110 | ~/.buildozer/android/platform/android-sdk/tools/bin/sdkmanager "build-tools;29.0.0" 111 | 112 | Then press "y" then enter to accept the license. 113 | 114 | Alternatively, the Android SDK license can be automatically accepted - see `build.spec` for details. 115 | 116 | 117 | python-for-android related errors 118 | """"""""""""""""""""""""""""""""" 119 | See the dedicated `p4a troubleshooting documentation 120 | `_. 121 | 122 | 123 | Targeting IOS 124 | ------------- 125 | 126 | Additional installation required to support iOS: 127 | 128 | * Install XCode and command line tools (through the AppStore) 129 | * Install `Homebrew `_:: 130 | 131 | brew install pkg-config sdl2 sdl2_image sdl2_ttf sdl2_mixer gstreamer autoconf automake 132 | 133 | * Install pip, virtualenv and Kivy for iOS:: 134 | 135 | python -m pip install --user --upgrade pip virtualenv kivy-ios 136 | 137 | -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | Let's get started with Buildozer! 5 | 6 | Init and build for Android 7 | -------------------------- 8 | 9 | #. Buildozer will try to guess the version of your application, by searching a 10 | line like `__version__ = "1.0.3"` in your `main.py`. Ensure you have one at 11 | the start of your application. It is not mandatory but heavily advised. 12 | 13 | #. Create a `buildozer.spec` file, with:: 14 | 15 | buildozer init 16 | 17 | #. Edit the `buildozer.spec` according to the :ref:`specifications`. You should 18 | at least change the `title`, `package.name` and `package.domain` in the 19 | `[app]` section. 20 | 21 | #. Start a Android/debug build with:: 22 | 23 | buildozer -v android debug 24 | 25 | #. Now it's time for a coffee / tea, or a dinner if you have a slow computer. 26 | The first build will be slow, as it will download the Android SDK, NDK, and 27 | others tools needed for the compilation. 28 | Don't worry, thoses files will be saved in a global directory and will be 29 | shared across the different project you'll manage with Buildozer. 30 | 31 | #. At the end, you should have an APK or AAB file in the `bin/` directory. 32 | 33 | 34 | Run my application 35 | ------------------ 36 | 37 | Buildozer is able to deploy the application on your mobile, run it, and even 38 | get back the log into the console. It will work only if you already compiled 39 | your application at least once:: 40 | 41 | buildozer android deploy run logcat 42 | 43 | For iOS, it would look the same:: 44 | 45 | buildozer ios deploy run 46 | 47 | You can combine the compilation with the deployment:: 48 | 49 | buildozer -v android debug deploy run logcat 50 | 51 | You can also set this line at the default command to do if Buildozer is started 52 | without any arguments:: 53 | 54 | buildozer setdefault android debug deploy run logcat 55 | 56 | # now just type buildozer, and it will do the default command 57 | buildozer 58 | 59 | To save the logcat output into a file named `my_log.txt` (the file will appear in your current directory):: 60 | 61 | buildozer -v android debug deploy run logcat > my_log.txt 62 | 63 | To see your running application's print() messages and python's error messages, use: 64 | 65 | :: 66 | 67 | buildozer -v android deploy run logcat | grep python 68 | 69 | Run my application from Windows 70 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 71 | 72 | - Plug your Android device on a USB port. 73 | 74 | - Open Windows PowerShell, go into the folder where you installed the Windows version of ADB, and activate the ADB daemon. When the daemon is started you must see a number besides the word "device" meaning your device was correctly detected. In case of trouble, try another USB port or USB cable. 75 | 76 | :: 77 | 78 | cd C:\platform-tools\ 79 | .\adb.exe devices 80 | 81 | - Open the Linux distribution you installed on Windows Subsystem for Linux (WSL) and proceed with the deploy commands: 82 | 83 | :: 84 | 85 | buildozer -v android deploy run 86 | 87 | It is important to notice that Windows ADB and Buildozer-installed ADB must be the same version. To check the versions, 88 | open PowerShell and type:: 89 | 90 | cd C:\platform-tools\ 91 | .\adb.exe version 92 | wsl 93 | cd ~/.buildozer/android/platform/android-sdk/platform-tools/ 94 | ./adb version 95 | 96 | Install on non-connected devices 97 | -------------------------------- 98 | 99 | If you have compiled a package, and want to share it easily with others 100 | devices, you might be interested with the `serve` command. It will serve the 101 | `bin/` directory over HTTP. Then you just have to access to the URL showed in 102 | the console from your mobile:: 103 | 104 | buildozer serve 105 | 106 | -------------------------------------------------------------------------------- /docs/source/recipes.rst: -------------------------------------------------------------------------------- 1 | Recipes 2 | ======= 3 | 4 | Python apps may depend on third party packages and extensions. 5 | 6 | Most packages are written in pure Python, and Buildozer can generally used them 7 | without any modification. 8 | 9 | However, some packages and Python extensions require modification to work on 10 | mobile platforms. 11 | 12 | For example, for extensions and packages that depend on C or other programming 13 | languages, the default compilation instructions may not work for the target; 14 | The ARM compiler and Android NDK introduce special requirements that the library 15 | may not handle correctly 16 | 17 | For such cases, a "recipe" is required. A recipe allows you to compile libraries 18 | and Python extension for the mobile by patching them before use. 19 | 20 | python-for-android and Kivy for iOS come, batteries included, with a number of 21 | recipes for the most popular packages. 22 | 23 | However, if you use a novel package - and there are no pure Python equivalents that 24 | you can substitute in - you may need to write (or commission) your own recipe. We 25 | would welcome your recipe as a contribution to the project to help the next developer 26 | who wants to use the same library. 27 | 28 | More instructions on how to write your own recipes is available in the 29 | `Kivy for iOS `_ and 30 | `python-for-android documentation `_. 31 | 32 | Instructions on how to test your own recipes from Buildozer is available in the 33 | `latest Buildozer Contribution Guidelines `_. 34 | -------------------------------------------------------------------------------- /docs/source/specifications.rst: -------------------------------------------------------------------------------- 1 | Specifications 2 | ============== 3 | 4 | This document explains in detail all the configuration tokens you can use in 5 | `buildozer.spec`. 6 | 7 | Section [app] 8 | ------------- 9 | 10 | - `title`: String, title of your application. 11 | 12 | It might be possible that some characters are not working depending on the 13 | targeted platform. It's best to try and see if everything works as expected. 14 | Try to avoid too long titles, as they will also not fit in the title 15 | displayed under the icon. 16 | 17 | - `package.name`: String, package name. 18 | 19 | The Package name is one word with only ASCII characters and/or numbers. It 20 | should not contain any special characters. For example, if your application 21 | is named `Flat Jewels`, the package name can be `flatjewels`. 22 | 23 | - `package.domain`: String, package domain. 24 | 25 | Package domain is a string that references the company or individual that 26 | did the app. Both domain+name will become your application identifier for 27 | Android and iOS, choose it carefully. As an example, when the Kivy`s team 28 | is publishing an application, the domain starts with `org.kivy`. 29 | 30 | - `source.dir`: String, location of your application sources. 31 | 32 | The location must be a directory that contains a `main.py` file. It defaults 33 | to the directory where `buildozer.spec` is. 34 | 35 | - Source Inclusion/Exclusion options. 36 | 37 | - `source.include_exts`: List, file extensions to include. 38 | - `source.exclude_exts`: List, file extensions to exclude, even if included by 39 | `source.include_exts` 40 | - `source.exclude_dirs`: List, directories to exclude. 41 | - `source.exclude_patterns`: List, files to exclude if they match a pattern. 42 | - `source.include_patterns`: List, files to include if they match a pattern, even if excluded by 43 | `source.exclude_dirs` or `source.exclude_patterns` 44 | 45 | By default, not all files are in your `source.dir` are included. You can 46 | use these options to alter which files are included in your app and which 47 | are excluded. 48 | 49 | Directories and files starting with a "." are always excluded; this cannot be 50 | overridden. 51 | 52 | Files that have an extension that is not in `source.include_exts` are excluded. 53 | (The default suggestion is `py,png,jpg,kv,atlas`. You may want to include other 54 | file extensions such as resource files: gif, xml, mp3, etc.) File names that 55 | have no extension (i.e contain no ".") are not excluded here. 56 | `source.exclude_exts` takes priority over `source.include_exts` - it excludes any listed extensions 57 | that were previously included. 58 | 59 | Files and directories in directories listed in `source.exclude_dirs` are excluded. For example, you can exclude your 60 | `tests` and `bin` directory with:: 61 | 62 | source.exclude_dirs = tests, bin 63 | 64 | `source.exclude_patterns` are also excluded. This is useful for excluding individual 65 | files. For example:: 66 | 67 | source.exclude_patterns = license 68 | 69 | These dir and pattern exclusions may be overridden with 70 | `source.include_patterns` - files and directories that match will once again be included. 71 | 72 | However, `source.include_patterns` does not override the `source.include_exts` nor 73 | `source.exclude_exts`. `source.include_patterns` also cannot be used to include files or directories that 74 | start with ".") 75 | 76 | - `version.regex`: Regex, Regular expression to capture the version in 77 | `version.filename`. 78 | 79 | The default capture method of your application version is by grepping a line 80 | like this:: 81 | 82 | __version__ = "1.0" 83 | 84 | The `1.0` will be used as a version. 85 | 86 | - `version.filename`: String, defaults to the main.py. 87 | 88 | File to use for capturing the version with `version.regex`. 89 | 90 | - `version`: String, manual application version. 91 | 92 | If you don't want to capture the version, comment out both `version.regex` 93 | and `version.filename`, then put the version you want directly in the 94 | `version` token:: 95 | 96 | # version.regex = 97 | # version.filename = 98 | version = 1.0 99 | 100 | - `requirements`: List, Python modules or extensions that your application 101 | requires. 102 | 103 | The requirements can be either a name of a recipe in the Python-for-android 104 | project, or a pure-Python package. For example, if your application requires 105 | Kivy and requests, you need to write:: 106 | 107 | requirements = kivy,requests 108 | 109 | If your application tries to install a Python extension (ie, a Python 110 | package that requires compilation), and the extension doesn't have a recipe 111 | associated to Python-for-android, it will not work. We explicitly disable 112 | the compilation here. If you want to make it work, contribute to the 113 | Python-for-android project by creating a recipe. See :doc:`contribute`. 114 | 115 | - `presplash.filename`: String, loading screen of your application. 116 | 117 | Presplash is the image shown on the device during application loading. 118 | It is called presplash on Android, and Loading image on iOS. The image might 119 | have different requirements depending the platform. Currently, Buildozer 120 | works well only with Android, iOS support is not great on this. 121 | 122 | The image must be a JPG or PNG, preferable with Power-of-two size, e.g., a 123 | 512x512 image is perfect to target all the devices. The image is not fitted, 124 | scaled, or anything on the device. If you provide a too-large image, it might 125 | not fit on small screens. 126 | 127 | - `icon.filename`: String, icon of your application. 128 | 129 | The icon of your application. It must be a PNG of 512x512 size to be able to 130 | cover all the various platform requirements. 131 | 132 | - `orientation`: List, supported orientations of the application. 133 | 134 | Indicate the orientations that your application supports. 135 | Valid values are: `portrait`, `landscape`, `portrait-reverse`, `landscape-reverse`. 136 | Defaults to `[landscape]`. 137 | 138 | - `fullscreen`: Boolean, fullscreen mode. 139 | 140 | Defaults to true, your application will run in fullscreen. Means the status 141 | bar will be hidden. If you want to let the user access the status bar, 142 | hour, notifications, use 0 as a value. 143 | 144 | - `home_app`: Boolean, Home App (launcher app) usage. 145 | 146 | Defaults to false, your application will be listed as a Home App (launcher app) if true. 147 | 148 | - `display_cutout`: String, display-cutout mode to be used. 149 | 150 | Defaults to `never`. Application will render around the cutout (notch) if set to either `default`, `shortEdges`. -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Buildozer 3 | ''' 4 | 5 | import sys 6 | from setuptools import setup 7 | from os.path import dirname, join 8 | import codecs 9 | import os 10 | import re 11 | import io 12 | 13 | here = os.path.abspath(os.path.dirname(__file__)) 14 | 15 | CURRENT_PYTHON = sys.version_info[:2] 16 | REQUIRED_PYTHON = (3, 8) 17 | 18 | # This check and everything above must remain compatible with Python 2.7. 19 | if CURRENT_PYTHON < REQUIRED_PYTHON: 20 | sys.stderr.write(""" 21 | ========================== 22 | Unsupported Python version 23 | ========================== 24 | This version of buildozer requires Python {}.{}, but you're trying to 25 | install it on Python {}.{}. 26 | """.format(*(REQUIRED_PYTHON + CURRENT_PYTHON))) 27 | sys.exit(1) 28 | 29 | 30 | def find_version(*file_paths): 31 | # Open in Latin-1 so that we avoid encoding errors. 32 | # Use codecs.open for Python 2 compatibility 33 | with codecs.open(os.path.join(here, *file_paths), 'r', 'utf-8') as f: 34 | version_file = f.read() 35 | 36 | # The version line must have the form 37 | # __version__ = 'ver' 38 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 39 | version_file, re.M) 40 | if version_match: 41 | return version_match.group(1) 42 | raise RuntimeError("Unable to find version string.") 43 | 44 | 45 | curdir = dirname(__file__) 46 | with io.open(join(curdir, "README.md"), encoding="utf-8") as fd: 47 | readme = fd.read() 48 | with io.open(join(curdir, "CHANGELOG.md"), encoding="utf-8") as fd: 49 | changelog = fd.read() 50 | 51 | setup( 52 | name='buildozer', 53 | version=find_version('buildozer', '__init__.py'), 54 | description='Turns Python applications into binary packages ready for ' 55 | 'installation on a number of platforms.', 56 | long_description=readme + "\n\n" + changelog, 57 | long_description_content_type='text/markdown', 58 | author='Mathieu Virbel', 59 | author_email='mat@kivy.org', 60 | url='https://github.com/kivy/buildozer', 61 | project_urls={ 62 | 'Website': "https://kivy.org", 63 | 'Documentation': "https://buildozer.readthedocs.io/en/stable/#", 64 | 'Source': "https://github.com/kivy/buildozer", 65 | 'Bug Reports': "https://github.com/kivy/buildozer/issues", 66 | }, 67 | license='MIT', 68 | packages=[ 69 | 'buildozer', 'buildozer.targets', 'buildozer.libs', 'buildozer.scripts' 70 | ], 71 | package_data={'buildozer': ['default.spec']}, 72 | include_package_data=True, 73 | install_requires=[ 74 | 'pexpect', 75 | 'packaging', 76 | # Cython is required by both kivy-ios and python-for-android. 77 | # However, python-for-android does not include it in its dependencies 78 | # and kivy-ios's dependencies are not always checked, so it is included 79 | # here. 80 | # Restricted version because python-for-android's recipes can't handle 81 | # later versions. 82 | 'cython<3.0' 83 | ], 84 | extras_require={ 85 | 'test': ['pytest'], 86 | 'docs': ['sphinx'], 87 | 'ios': ['kivy-ios'], 88 | }, 89 | classifiers=[ 90 | 'Development Status :: 5 - Production/Stable', 91 | 'Intended Audience :: Developers', 92 | 'Topic :: Software Development :: Build Tools', 93 | 'Programming Language :: Python :: 3', 94 | 'Programming Language :: Python :: 3.8', 95 | 'Programming Language :: Python :: 3.9', 96 | 'Programming Language :: Python :: 3.10', 97 | 'Programming Language :: Python :: 3.11', 98 | ], 99 | entry_points={ 100 | 'console_scripts': [ 101 | 'buildozer=buildozer.scripts.client:main', 102 | 'buildozer-remote=buildozer.scripts.remote:main' 103 | ] 104 | }) 105 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kivy/buildozer/abc2d7e66c8abe096a95ed58befe6617f7efdad0/tests/__init__.py -------------------------------------------------------------------------------- /tests/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kivy/buildozer/abc2d7e66c8abe096a95ed58befe6617f7efdad0/tests/scripts/__init__.py -------------------------------------------------------------------------------- /tests/scripts/test_client.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | from unittest import mock 4 | 5 | from buildozer.exceptions import BuildozerCommandException 6 | from buildozer.scripts import client 7 | 8 | 9 | class TestClient(unittest.TestCase): 10 | 11 | def test_run_command_called(self): 12 | """ 13 | Checks Buildozer.run_command() is being called with arguments from command line. 14 | """ 15 | with mock.patch('buildozer.Buildozer.run_command') as m_run_command: 16 | client.main() 17 | assert m_run_command.call_args_list == [mock.call(sys.argv[1:])] 18 | 19 | def test_exit_code(self): 20 | """ 21 | Makes sure the CLI exits with error code on BuildozerCommandException, refs #674. 22 | """ 23 | with mock.patch('buildozer.Buildozer.run_command') as m_run_command: 24 | m_run_command.side_effect = BuildozerCommandException() 25 | with self.assertRaises(SystemExit) as context: 26 | client.main() 27 | assert context.exception.code == 1 28 | -------------------------------------------------------------------------------- /tests/targets/test_ios.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import sys 3 | import tempfile 4 | from unittest import mock 5 | 6 | import pytest 7 | 8 | from buildozer.buildops import CommandResult 9 | from buildozer.exceptions import BuildozerCommandException 10 | from buildozer.targets.ios import TargetIos 11 | from tests.targets.utils import ( 12 | init_buildozer, 13 | patch_buildops_checkbin, 14 | patch_buildops_cmd, 15 | patch_buildops_file_exists, 16 | patch_logger_error, 17 | ) 18 | 19 | 20 | def patch_target_ios(method): 21 | return mock.patch("buildozer.targets.ios.TargetIos.{method}".format(method=method)) 22 | 23 | 24 | def init_target(temp_dir, options=None): 25 | buildozer = init_buildozer(temp_dir, "ios", options) 26 | return TargetIos(buildozer) 27 | 28 | 29 | @pytest.mark.skipif( 30 | sys.platform != "darwin", reason="Only macOS is supported for target iOS" 31 | ) 32 | class TestTargetIos: 33 | def setup_method(self): 34 | """ 35 | Create a temporary directory that will contain the spec file and will 36 | serve as the root_dir. 37 | """ 38 | self.temp_dir = tempfile.TemporaryDirectory() 39 | 40 | def tear_method(self): 41 | """ 42 | Remove the temporary directory created in self.setup_method. 43 | """ 44 | self.temp_dir.cleanup() 45 | 46 | def test_init(self): 47 | """Tests init defaults.""" 48 | target = init_target(self.temp_dir) 49 | assert target.targetname == "ios" 50 | assert target.code_signing_allowed == "CODE_SIGNING_ALLOWED=NO" 51 | assert target.build_mode == "debug" 52 | assert target.platform_update is False 53 | 54 | def test_check_requirements(self): 55 | """Basic tests for the check_requirements() method.""" 56 | target = init_target(self.temp_dir) 57 | assert not hasattr(target, "javac_cmd") 58 | with patch_buildops_checkbin() as m_checkbin: 59 | target.check_requirements() 60 | assert m_checkbin.call_args_list == [ 61 | mock.call("Xcode xcodebuild", "xcodebuild"), 62 | mock.call("Xcode xcode-select", "xcode-select"), 63 | mock.call("Git git", "git"), 64 | mock.call("Cython cython", "cython"), 65 | mock.call("pkg-config", "pkg-config"), 66 | mock.call("autoconf", "autoconf"), 67 | mock.call("automake", "automake"), 68 | mock.call("libtool", "libtool"), 69 | ] 70 | assert target._toolchain_cmd[-1] == "toolchain.py" 71 | assert target._xcodebuild_cmd == ["xcodebuild"] 72 | 73 | def test_check_configuration_tokens(self): 74 | """Basic tests for the check_configuration_tokens() method.""" 75 | target = init_target(self.temp_dir, {"ios.codesign.allowed": "yes"}) 76 | with mock.patch( 77 | "buildozer.targets.android.Target.check_configuration_tokens" 78 | ) as m_check_configuration_tokens, mock.patch( 79 | "buildozer.targets.ios.TargetIos._get_available_identities" 80 | ) as m_get_available_identities: 81 | target.check_configuration_tokens() 82 | assert m_get_available_identities.call_args_list == [mock.call()] 83 | assert m_check_configuration_tokens.call_args_list == [ 84 | mock.call( 85 | [ 86 | '[app] "ios.codesign.debug" key missing, you must give a certificate name to use.', 87 | '[app] "ios.codesign.release" key missing, you must give a certificate name to use.', 88 | ] 89 | ) 90 | ] 91 | 92 | def test_get_available_packages(self): 93 | """Checks the toolchain `recipes --compact` output is parsed correctly to return recipe list.""" 94 | target = init_target(self.temp_dir) 95 | with patch_target_ios("toolchain") as m_toolchain: 96 | m_toolchain.return_value = ("hostpython3 kivy pillow python3 sdl2", None, 0) 97 | available_packages = target.get_available_packages() 98 | assert m_toolchain.call_args_list == [ 99 | mock.call(["recipes", "--compact"], get_stdout=True) 100 | ] 101 | assert available_packages == [ 102 | "hostpython3", 103 | "kivy", 104 | "pillow", 105 | "python3", 106 | "sdl2", 107 | ] 108 | 109 | def test_install_platform(self): 110 | """Checks `install_platform()` calls clone commands and sets `ios_dir` and `ios_deploy_dir` attributes.""" 111 | target = init_target(self.temp_dir) 112 | assert target.ios_dir is None 113 | assert target.ios_deploy_dir is None 114 | with patch_buildops_cmd() as m_cmd: 115 | target.install_platform() 116 | assert m_cmd.call_args_list == [ 117 | mock.call( 118 | [ 119 | "git", 120 | "clone", 121 | "--branch", 122 | "master", 123 | "https://github.com/kivy/kivy-ios", 124 | ], 125 | cwd=mock.ANY, 126 | env=mock.ANY, 127 | ), 128 | mock.call( 129 | [ 130 | "git", 131 | "clone", 132 | "--branch", 133 | "1.12.2", 134 | "https://github.com/phonegap/ios-deploy", 135 | ], 136 | cwd=mock.ANY, 137 | env=mock.ANY, 138 | ), 139 | ] 140 | assert target.ios_dir.endswith(".buildozer/ios/platform/kivy-ios") 141 | assert target.ios_deploy_dir.endswith(".buildozer/ios/platform/ios-deploy") 142 | 143 | def test_compile_platform(self): 144 | """Checks the `toolchain build` command is called on the ios requirements.""" 145 | target = init_target(self.temp_dir) 146 | target.ios_deploy_dir = "/ios/deploy/dir" 147 | # fmt: off 148 | with patch_target_ios("get_available_packages") as m_get_available_packages, \ 149 | patch_target_ios("toolchain") as m_toolchain, \ 150 | patch_buildops_file_exists() as m_file_exists: 151 | m_get_available_packages.return_value = ["hostpython3", "python3"] 152 | m_file_exists.return_value = True 153 | target.compile_platform() 154 | # fmt: on 155 | assert m_get_available_packages.call_args_list == [mock.call()] 156 | assert m_toolchain.call_args_list == [mock.call(["build", "python3"])] 157 | assert m_file_exists.call_args_list == [ 158 | mock.call(os.path.join(target.ios_deploy_dir, "ios-deploy")) 159 | ] 160 | 161 | def test_get_package(self): 162 | """Checks default package values and checks it can be overridden.""" 163 | # default value 164 | target = init_target(self.temp_dir) 165 | package = target._get_package() 166 | assert package == "org.test.myapp" 167 | # override 168 | target = init_target( 169 | self.temp_dir, 170 | {"package.domain": "com.github.kivy", "package.name": "buildozer"}, 171 | ) 172 | package = target._get_package() 173 | assert package == "com.github.kivy.buildozer" 174 | 175 | def test_unlock_keychain_wrong_password(self): 176 | """A `BuildozerCommandException` should be raised on wrong password 3 times.""" 177 | target = init_target(self.temp_dir) 178 | # fmt: off 179 | with mock.patch("buildozer.targets.ios.getpass") as m_getpass, \ 180 | patch_buildops_cmd() as m_cmd, \ 181 | pytest.raises(BuildozerCommandException): 182 | m_getpass.return_value = "password" 183 | # the `security unlock-keychain` command returned an error 184 | # hence we'll get prompted to enter the password 185 | m_cmd.return_value = CommandResult(None, None, 123) 186 | target._unlock_keychain() 187 | # fmt: on 188 | assert m_getpass.call_args_list == [ 189 | mock.call("Password to unlock the default keychain:"), 190 | mock.call("Password to unlock the default keychain:"), 191 | mock.call("Password to unlock the default keychain:"), 192 | ] 193 | 194 | def test_build_package_no_signature(self): 195 | """Code signing is currently required to go through final `xcodebuild` step.""" 196 | target = init_target(self.temp_dir) 197 | target.ios_dir = "/ios/dir" 198 | # fmt: off 199 | with patch_target_ios("_unlock_keychain") as m_unlock_keychain, \ 200 | patch_logger_error() as m_error, \ 201 | mock.patch("buildozer.targets.ios.TargetIos.load_plist_from_file") as m_load_plist_from_file, \ 202 | mock.patch("buildozer.targets.ios.TargetIos.dump_plist_to_file") as m_dump_plist_to_file, \ 203 | patch_buildops_cmd() as m_cmd: 204 | m_load_plist_from_file.return_value = {} 205 | target.build_package() 206 | # fmt: on 207 | assert m_unlock_keychain.call_args_list == [mock.call()] 208 | assert m_error.call_args_list == [ 209 | mock.call( 210 | "Cannot create the IPA package without signature. " 211 | 'You must fill the "ios.codesign.debug" token.' 212 | ) 213 | ] 214 | assert m_load_plist_from_file.call_args_list == [ 215 | mock.call("/ios/dir/myapp-ios/myapp-Info.plist") 216 | ] 217 | assert m_dump_plist_to_file.call_args_list == [ 218 | mock.call( 219 | { 220 | "CFBundleIdentifier": "org.test.myapp", 221 | "CFBundleShortVersionString": "0.1", 222 | "CFBundleVersion": "0.1.None", 223 | }, 224 | "/ios/dir/myapp-ios/myapp-Info.plist", 225 | ) 226 | ] 227 | assert m_cmd.call_args_list == [ 228 | mock.call(mock.ANY, cwd=target.ios_dir, env=mock.ANY), 229 | mock.call([ 230 | "xcodebuild", 231 | "-configuration", 232 | "Debug", 233 | "-allowProvisioningUpdates", 234 | "ENABLE_BITCODE=NO", 235 | "CODE_SIGNING_ALLOWED=NO", 236 | "clean", 237 | "build"], 238 | cwd="/ios/dir/myapp-ios", 239 | env=mock.ANY, 240 | ), 241 | mock.call([ 242 | "xcodebuild", 243 | "-alltargets", 244 | "-configuration", 245 | "Debug", 246 | "-scheme", 247 | "myapp", 248 | "-archivePath", 249 | "/ios/dir/myapp-0.1.intermediates/myapp-0.1.xcarchive", 250 | "-destination", 251 | "generic/platform=iOS", 252 | "archive", 253 | "ENABLE_BITCODE=NO", 254 | "CODE_SIGNING_ALLOWED=NO"], 255 | cwd="/ios/dir/myapp-ios", 256 | env=mock.ANY, 257 | ), 258 | ] 259 | -------------------------------------------------------------------------------- /tests/targets/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from unittest import mock 4 | 5 | import buildozer as buildozer_module 6 | from buildozer import Buildozer 7 | 8 | 9 | def patch_buildops_cmd(): 10 | return mock.patch("buildozer.buildops.cmd") 11 | 12 | 13 | def patch_buildops_checkbin(): 14 | return mock.patch("buildozer.buildops.checkbin") 15 | 16 | 17 | def patch_buildops_file_exists(): 18 | return mock.patch("buildozer.buildops.file_exists") 19 | 20 | 21 | def patch_logger_error(): 22 | return mock.patch("buildozer.logger.Logger.error") 23 | 24 | 25 | def default_specfile_path(): 26 | return os.path.join(os.path.dirname(buildozer_module.__file__), "default.spec") 27 | 28 | 29 | def init_buildozer(temp_dir, target, options=None): 30 | """ 31 | Create a buildozer.spec file in the temporary directory and init the 32 | Buildozer instance. 33 | 34 | The optional argument can be used to overwrite the config options in 35 | the buildozer.spec file, e.g.: 36 | 37 | init_buildozer({'title': 'Test App'}) 38 | 39 | will replace line 4 of the default spec file. 40 | """ 41 | if options is None: 42 | options = {} 43 | 44 | spec_path = os.path.join(temp_dir.name, "buildozer.spec") 45 | 46 | with open(default_specfile_path()) as f: 47 | default_spec = f.readlines() 48 | 49 | spec = [] 50 | for line in default_spec: 51 | if line.strip(): 52 | match = re.search(r"[#\s]?([0-9a-z_.]+)", line) 53 | key = match and match.group(1) 54 | if key in options: 55 | line = "{} = {}\n".format(key, options[key]) 56 | 57 | spec.append(line) 58 | 59 | with open(spec_path, "w") as f: 60 | f.writelines(spec) 61 | 62 | return Buildozer(filename=spec_path, target=target) 63 | -------------------------------------------------------------------------------- /tests/test_buildozer.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import codecs 4 | import unittest 5 | import buildozer as buildozer_module 6 | from buildozer import Buildozer 7 | from io import StringIO 8 | from sys import platform 9 | import tempfile 10 | from unittest import mock 11 | 12 | from buildozer.targets.android import ( 13 | TargetAndroid, DEFAULT_ANDROID_NDK_VERSION, MSG_P4A_RECOMMENDED_NDK_ERROR 14 | ) 15 | 16 | 17 | class TestBuildozer(unittest.TestCase): 18 | 19 | def setUp(self): 20 | """ 21 | Creates a temporary spec file containing the content of the default.spec. 22 | """ 23 | self.specfile = tempfile.NamedTemporaryFile(suffix='.spec', delete=False) 24 | self.specfilename = self.specfile.name 25 | default_spec = codecs.open(self.default_specfile_path(), encoding='utf-8') 26 | self.specfile.write(default_spec.read().encode('utf-8')) 27 | self.specfile.close() 28 | 29 | def tearDown(self): 30 | """ 31 | Deletes the temporary spec file. 32 | """ 33 | os.unlink(self.specfile.name) 34 | 35 | @staticmethod 36 | def default_specfile_path(): 37 | return os.path.join( 38 | os.path.dirname(buildozer_module.__file__), 39 | 'default.spec') 40 | 41 | @staticmethod 42 | def file_re_sub(filepath, pattern, replace): 43 | """ 44 | Helper method for inplace file regex editing. 45 | """ 46 | with open(filepath) as f: 47 | file_content = f.read() 48 | file_content = re.sub(pattern, replace, file_content) 49 | with open(filepath, 'w') as f: 50 | f.write(file_content) 51 | 52 | @classmethod 53 | def set_specfile_log_level(cls, specfilename, log_level): 54 | """ 55 | Helper method for setting `log_level` in a given `specfilename`. 56 | """ 57 | pattern = 'log_level = [0-9]' 58 | replace = 'log_level = {}'.format(log_level) 59 | cls.file_re_sub(specfilename, pattern, replace) 60 | buildozer = Buildozer(specfilename) 61 | assert buildozer.logger.log_level == log_level 62 | 63 | def test_buildozer_base(self): 64 | """ 65 | Basic test making sure the Buildozer object can be instantiated. 66 | """ 67 | buildozer = Buildozer() 68 | assert buildozer.specfilename == 'buildozer.spec' 69 | # spec file doesn't have to exist 70 | assert os.path.exists(buildozer.specfilename) is False 71 | 72 | def test_buildozer_read_spec(self): 73 | """ 74 | Initializes Buildozer object from existing spec file. 75 | """ 76 | buildozer = Buildozer(filename=self.default_specfile_path()) 77 | assert os.path.exists(buildozer.specfilename) is True 78 | 79 | def test_buildozer_help(self): 80 | """ 81 | Makes sure the help gets display with no error, refs: 82 | https://github.com/kivy/buildozer/issues/813 83 | """ 84 | buildozer = Buildozer() 85 | with mock.patch('sys.stdout', new_callable=StringIO) as mock_stdout: 86 | buildozer.usage() 87 | assert 'Usage:' in mock_stdout.getvalue() 88 | 89 | def test_log_get_set(self): 90 | """ 91 | Tests reading and setting log level from spec file. 92 | """ 93 | # the default log level value is known 94 | buildozer = Buildozer('does_not_exist.spec') 95 | assert buildozer.logger.log_level == 2 96 | # sets log level to 1 on the spec file 97 | self.set_specfile_log_level(self.specfile.name, 1) 98 | buildozer = Buildozer(self.specfile.name) 99 | assert buildozer.logger.log_level == 1 100 | 101 | def test_run_command_unknown(self): 102 | """ 103 | Makes sure the unknown command/target is handled gracefully, refs: 104 | https://github.com/kivy/buildozer/issues/812 105 | """ 106 | buildozer = Buildozer() 107 | command = 'foobar' 108 | args = [command, 'debug'] 109 | with mock.patch('sys.stdout', new_callable=StringIO) as mock_stdout: 110 | with self.assertRaises(SystemExit): 111 | buildozer.run_command(args) 112 | assert mock_stdout.getvalue() == 'Unknown command/target {}\n'.format(command) 113 | 114 | @unittest.skipIf( 115 | platform == "win32", 116 | "Test can't handle when resulting path is normalised on Windows") 117 | def test_android_ant_path(self): 118 | """ 119 | Verify that the selected ANT path is being used from the spec file 120 | """ 121 | my_ant_path = '/my/ant/path' 122 | 123 | buildozer = Buildozer(filename=self.default_specfile_path(), target='android') 124 | buildozer.config.set('app', 'android.ant_path', my_ant_path) # Set ANT path 125 | target = TargetAndroid(buildozer=buildozer) 126 | 127 | # Mock first run 128 | with mock.patch('buildozer.buildops.download') as download, \ 129 | mock.patch('buildozer.buildops.file_extract') as m_file_extract, \ 130 | mock.patch('os.makedirs'): 131 | ant_path = target._install_apache_ant() 132 | assert m_file_extract.call_args_list == [ 133 | mock.call(mock.ANY, cwd='/my/ant/path', env=mock.ANY)] 134 | assert ant_path == my_ant_path 135 | assert download.call_args_list == [ 136 | mock.call("https://archive.apache.org/dist/ant/binaries/", mock.ANY, cwd=my_ant_path)] 137 | # Mock ant already installed 138 | with mock.patch('buildozer.buildops.file_exists', return_value=True): 139 | ant_path = target._install_apache_ant() 140 | assert ant_path == my_ant_path 141 | 142 | def test_p4a_recommended_ndk_version_default_value(self): 143 | self.set_specfile_log_level(self.specfile.name, 1) 144 | buildozer = Buildozer(self.specfile.name, 'android') 145 | assert buildozer.target.p4a_recommended_ndk_version is None 146 | 147 | def test_p4a_recommended_android_ndk_error(self): 148 | self.set_specfile_log_level(self.specfile.name, 1) 149 | buildozer = Buildozer(self.specfile.name, 'android') 150 | 151 | with mock.patch('sys.stdout', new_callable=StringIO) as mock_stdout: 152 | ndk_version = buildozer.target.p4a_recommended_android_ndk 153 | assert MSG_P4A_RECOMMENDED_NDK_ERROR in mock_stdout.getvalue() 154 | # and we should get the default android's ndk version of buildozer 155 | assert ndk_version == DEFAULT_ANDROID_NDK_VERSION 156 | 157 | @mock.patch('buildozer.targets.android.os.path.isfile') 158 | @mock.patch('buildozer.targets.android.os.path.exists') 159 | @mock.patch('buildozer.targets.android.open', create=True) 160 | def test_p4a_recommended_android_ndk_found( 161 | self, mock_open, mock_exists, mock_isfile 162 | ): 163 | self.set_specfile_log_level(self.specfile.name, 1) 164 | buildozer = Buildozer(self.specfile.name, 'android') 165 | expected_ndk = '19b' 166 | recommended_line = 'RECOMMENDED_NDK_VERSION = {expected_ndk}\n'.format( 167 | expected_ndk=expected_ndk) 168 | mock_open.return_value = StringIO(recommended_line) 169 | ndk_version = buildozer.target.p4a_recommended_android_ndk 170 | p4a_dir = os.path.join( 171 | buildozer.platform_dir, buildozer.target.p4a_directory_name) 172 | mock_open.assert_called_once_with( 173 | os.path.join(p4a_dir, "pythonforandroid", "recommendations.py"), 'r' 174 | ) 175 | assert ndk_version == expected_ndk 176 | 177 | # now test that we only read one time p4a file, so we call again to 178 | # `p4a_recommended_android_ndk` and we should still have one call to `open` 179 | # file, the performed above 180 | ndk_version = buildozer.target.p4a_recommended_android_ndk 181 | mock_open.assert_called_once() 182 | -------------------------------------------------------------------------------- /tests/test_logger.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from buildozer.logger import Logger 3 | 4 | from io import StringIO 5 | from unittest import mock 6 | 7 | 8 | class TestLogger(unittest.TestCase): 9 | def test_log_print(self): 10 | """ 11 | Checks logger prints different info depending on log level. 12 | """ 13 | logger = Logger() 14 | 15 | # Test ERROR Level 16 | Logger.set_level(0) 17 | assert logger.log_level == logger.ERROR 18 | 19 | # at this level, only error messages should be printed 20 | with mock.patch("sys.stdout", new_callable=StringIO) as mock_stdout: 21 | logger.debug("debug message") 22 | logger.info("info message") 23 | logger.error("error message") 24 | # using `in` keyword rather than `==` because of color prefix/suffix 25 | assert "debug message" not in mock_stdout.getvalue() 26 | assert "info message" not in mock_stdout.getvalue() 27 | assert "error message" in mock_stdout.getvalue() 28 | 29 | # Test INFO Level 30 | Logger.set_level(1) 31 | assert logger.log_level == logger.INFO 32 | 33 | # at this level, debug messages should not be printed 34 | with mock.patch("sys.stdout", new_callable=StringIO) as mock_stdout: 35 | logger.debug("debug message") 36 | logger.info("info message") 37 | logger.error("error message") 38 | # using `in` keyword rather than `==` because of color prefix/suffix 39 | assert "debug message" not in mock_stdout.getvalue() 40 | assert "info message" in mock_stdout.getvalue() 41 | assert "error message" in mock_stdout.getvalue() 42 | 43 | # sets log level to 2 in the spec file 44 | Logger.set_level(2) 45 | assert logger.log_level == logger.DEBUG 46 | # at this level all message types should be printed 47 | with mock.patch("sys.stdout", new_callable=StringIO) as mock_stdout: 48 | logger.debug("debug message") 49 | logger.info("info message") 50 | logger.error("error message") 51 | assert "debug message" in mock_stdout.getvalue() 52 | assert "info message" in mock_stdout.getvalue() 53 | assert "error message" in mock_stdout.getvalue() 54 | -------------------------------------------------------------------------------- /tests/test_specparser.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | from pathlib import Path 3 | from tempfile import TemporaryDirectory 4 | import unittest 5 | 6 | from buildozer.specparser import SpecParser 7 | 8 | 9 | class TestSpecParser(unittest.TestCase): 10 | def test_overrides(self): 11 | environ["SECTION_1_ATTRIBUTE_1"] = "Env Value" 12 | 13 | # Test as a string. 14 | sp = SpecParser() 15 | sp.read_string( 16 | """ 17 | [section.1] 18 | attribute.1=String Value 19 | """ 20 | ) 21 | assert sp.get("section.1", "attribute.1") == "Env Value" 22 | 23 | # Test as a dict 24 | sp = SpecParser() 25 | sp.read_dict({"section.1": {"attribute.1": "Dict Value"}}) 26 | assert sp.get("section.1", "attribute.1") == "Env Value" 27 | 28 | with TemporaryDirectory() as temp_dir: 29 | spec_path = Path(temp_dir) / "test.spec" 30 | with open(spec_path, "w") as spec_file: 31 | spec_file.write( 32 | """ 33 | [section.1] 34 | attribute.1=File Value 35 | """ 36 | ) 37 | 38 | # Test as a file 39 | sp = SpecParser() 40 | with open(spec_path, "r") as spec_file: 41 | sp.read_file(spec_file) 42 | assert sp.get("section.1", "attribute.1") == "Env Value" 43 | 44 | # Test as a list of filenames 45 | sp = SpecParser() 46 | sp.read([spec_path]) 47 | assert sp.get("section.1", "attribute.1") == "Env Value" 48 | 49 | del environ["SECTION_1_ATTRIBUTE_1"] 50 | 51 | def test_new_getters(self): 52 | sp = SpecParser() 53 | sp.read_string( 54 | """ 55 | [section1] 56 | attribute1=1 57 | attribute2=red, white, blue 58 | attribute3=True 59 | attribute5=large/medium/small 60 | 61 | [section2:attribute4] 62 | red=1 63 | amber= 64 | green=3 65 | 66 | 67 | """ 68 | ) 69 | 70 | assert sp.get("section1", "attribute1") == "1" 71 | assert sp.getlist("section1", "attribute2") == ["red", "white", "blue"] 72 | assert sp.getlist("section1", "attribute2", strip=False) == [ 73 | "red", 74 | " white", 75 | " blue", 76 | ] 77 | 78 | assert sp.getlist("section2", "attribute4") == [ 79 | "red", 80 | "amber", 81 | "green", 82 | ] 83 | # Test with_values and section_sep 84 | assert sp.getlistvalues("section2", "attribute4") == [ 85 | "red=1", 86 | "amber=", 87 | "green=3", 88 | ] 89 | assert sp.getlist( 90 | "section2", "attribute4", with_values=True, section_sep=":" 91 | ) == [ 92 | "red:1", 93 | "amber:", 94 | "green:3", 95 | ] 96 | # Test split_char 97 | assert sp.getlist("section1", "attribute5", with_values=True) == [ 98 | "large/medium/small", 99 | ] 100 | assert sp.getlist( 101 | "section1", "attribute5", with_values=True, split_char="/" 102 | ) == [ 103 | "large", 104 | "medium", 105 | "small", 106 | ] 107 | 108 | assert sp.getbooldefault("section1", "attribute3") is True 109 | 110 | def test_case_sensitivity(self): 111 | sp = SpecParser() 112 | sp.read_string( 113 | """ 114 | [section1] 115 | attribute1=a 116 | Attribute1=A 117 | """ 118 | ) 119 | 120 | assert sp.get("section1", "attribute1") == "a" 121 | assert sp.get("section1", "Attribute1") == "A" 122 | 123 | def test_profiles(self): 124 | sp = SpecParser() 125 | sp.read_string( 126 | """ 127 | [section1] 128 | attribute1=full system 129 | [section1 @demo1, demo2] 130 | attribute1=demo mode 131 | """ 132 | ) 133 | 134 | # Before a profile is set, return the basic version. 135 | assert sp.get("section1", "attribute1") == "full system" 136 | 137 | # Empty profile makes no difference. 138 | sp.apply_profile(None) 139 | assert sp.get("section1", "attribute1") == "full system" 140 | 141 | # Inapplicable profile makes no difference 142 | sp.apply_profile("doesn't exist") 143 | assert sp.get("section1", "attribute1") == "full system" 144 | 145 | # Applicable profile changes value 146 | sp.apply_profile("demo2") 147 | assert sp.get("section1", "attribute1") == "demo mode" 148 | 149 | def test_profiles_vs_env_var(self): 150 | sp = SpecParser() 151 | 152 | environ["SECTION1_ATTRIBUTE1"] = "simulation mode" 153 | 154 | sp.read_string( 155 | """ 156 | [section1] 157 | attribute1=full system 158 | [section1@demo1,demo2] 159 | attribute1=demo mode 160 | """ 161 | ) 162 | 163 | # Before a profile is set, env var should win. 164 | assert sp.get("section1", "attribute1") == "simulation mode" 165 | 166 | # Applicable profile: env var should still win 167 | sp.apply_profile("demo1") 168 | assert sp.get("section1", "attribute1") == "simulation mode" 169 | 170 | del environ["SECTION1_ATTRIBUTE1"] 171 | 172 | def test_controversial_cases(self): 173 | """Some aspects of the config syntax seem to cause confusion. 174 | This shows what the code is *specified* to do, which might not be 175 | expected. 176 | """ 177 | sp = SpecParser() 178 | sp.read_string( 179 | """ 180 | [section] 181 | # Comments can be indented. 182 | option1=a # This is not considered a comment 183 | option2=this is 184 | a multiline string (not a list!) 185 | # This is considered a comment. 186 | this_is_not_an_option=it is still part of the multiline 187 | option3=this, is, one, way, of, representing, lists 188 | 189 | [section:option4] 190 | this_is 191 | another_way 192 | # This is a comment. 193 | of # This is not a comment. 194 | representing=4 195 | lists 196 | """ 197 | ) 198 | 199 | assert ( 200 | sp.get("section", "option1") == 201 | "a # This is not considered a comment" 202 | ) 203 | assert ( 204 | sp.get("section", "option2") == 205 | "this is\na multiline string (not a list!)\n" 206 | "this_is_not_an_option=it is still part of the multiline" 207 | ) 208 | assert sp.getlist("section", "option3") == [ 209 | "this", 210 | "is", 211 | "one", 212 | "way", 213 | "of", 214 | "representing", 215 | "lists", 216 | ] 217 | assert sp.getlist("section", "option4") == [ 218 | "this_is", 219 | "another_way", 220 | "of # This is not a comment.", 221 | "representing", 222 | "lists", 223 | ] 224 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = pep8 3 | 4 | [testenv] 5 | deps = 6 | pytest 7 | coverage 8 | commands = 9 | pytest tests/ 10 | coverage run --branch --source=buildozer -m pytest {posargs:tests/} 11 | coverage report -m 12 | 13 | [testenv:pep8] 14 | deps = flake8 15 | commands = flake8 buildozer/ tests/ 16 | 17 | [flake8] 18 | ignore = 19 | # continuation line missing indentation or outdented 20 | E122, 21 | # continuation line over-indented for hanging indent 22 | E126, 23 | # continuation line over-indented for visual indent 24 | E127, 25 | # continuation line under-indented for visual indent 26 | E128, 27 | # continuation line unaligned for hanging indent 28 | E131, 29 | # module level import not at top of file 30 | E402, 31 | # line too long 32 | E501, 33 | # do not use bare 'except' 34 | E722, 35 | # line break before binary operator 36 | W503, 37 | # line break after binary operator 38 | W504 39 | --------------------------------------------------------------------------------