{"id":18756469,"url":"https://github.com/octoprint/custopizer","last_synced_at":"2025-04-06T04:11:34.039Z","repository":{"id":39797763,"uuid":"381027552","full_name":"OctoPrint/CustoPiZer","owner":"OctoPrint","description":"A customization tool for Raspberry Pi OS images like OctoPi","archived":false,"fork":false,"pushed_at":"2025-03-03T09:15:22.000Z","size":85,"stargazers_count":95,"open_issues_count":1,"forks_count":21,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-30T03:05:49.400Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/OctoPrint.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-06-28T12:43:04.000Z","updated_at":"2025-03-28T16:56:28.000Z","dependencies_parsed_at":"2024-01-16T20:29:57.090Z","dependency_job_id":"f5f65d53-59a1-4bc7-9135-028a443aabf6","html_url":"https://github.com/OctoPrint/CustoPiZer","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OctoPrint%2FCustoPiZer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OctoPrint%2FCustoPiZer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OctoPrint%2FCustoPiZer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OctoPrint%2FCustoPiZer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/OctoPrint","download_url":"https://codeload.github.com/OctoPrint/CustoPiZer/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247430871,"owners_count":20937874,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-07T17:36:45.374Z","updated_at":"2025-04-06T04:11:34.018Z","avatar_url":"https://github.com/OctoPrint.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# CustoPiZer\n\n*A customization tool for Raspberry Pi OS images like OctoPi*\n\nCustoPiZer is based on work done as part of the amazing [CustomPiOS](https://github.com/guysoft/CustomPiOS) and \n[OctoPi](https://github.com/guysoft/OctoPi) build scripts maintained by [Guy Sheffer](https://github.com/guysoft).\n\nIt allows to customize an OS image with a set of scripts that are run on the mounted image inside a qemu'd chroot. This is useful\nto modify an existing image, e.g. to install additional software, prior to distributing it. The image is not booted, so unless the\nscripts itself do anything that generate unique files, the image will stay shareable without the risk of sharing hard coded secrets,\nkeys or similar.\n\nCustoPiZer was built for customization of OctoPi images by end users and vendors. It should however also work for generic images and\ntheir customization. YMMV.\n\n## Usage\n\nCreate a local workspace directory, place an image file therein named `input.img` and `scripts` containing your customization scripts.\nIf you need to make additional files available inside the image during build, place them inside `scripts/files` -- they will be mounted\ninside the image build under `/files`. Then run CustoPiZer via Docker:\n\n```\ndocker run --rm --privileged -v /path/to/workspace:/CustoPiZer/workspace ghcr.io/octoprint/custopizer:latest\n```\n\nYour customized image will be located in the `workspace` directory and named `output.img`.\n\nIf you are having problems getting the container to connect to the internet for updates etc. then run with `--dns 8.8.8.8`.\n\n### Why the `--privileged` flag?\n\nCustoPiZer uses loopback mounts to mount the image partitions. Those don't seem to work in an unprivileged container. Happy to \nget info on how to circumvent this problem.\n\n### Configuration\n\nThere are some configuration settings you can override by mounting a `config.local` file as `/CustoPiZer/config.local`. For the\navailable config settings, please take a look into the `EDITBASE_` variables in `src/config`.\n\nIf for example you want to override the enlarge and shrink sizes for the image build, mount something like this as `/CustoPiZer/config.local`:\n\n``` bash\n# enlarge image by 100MB prior to customization\nEDITBASE_IMAGE_ENLARGEROOT=100\n\n# shrink image to minimum size plus 20MB after customization\nEDITBASE_IMAGE_RESIZEROOT=20\n```\n\nThis can be achieved through `-v /path/to/config.local:/CustoPiZer/config.local` in the docker command, e.g.\n\n```\ndocker run --rm --privileged -v /path/to/workspace:/CustoPiZer/workspace -v /path/to/config.local:/CustoPiZer/config.local ghcr.io/octoprint/custopizer:latest\n```\n\n### Example\n\nPlace this in `workspace/scripts/01-update-octoprint`:\n\n``` bash\nset -x\nset -e\n\nexport LC_ALL=C\n\nsource /common.sh\ninstall_cleanup_trap\n\nsudo -u pi /home/pi/oprint/bin/pip install -U OctoPrint\n```\n\nPlace the image of the current [OctoPi release](https://octoprint.org/download) as `input.img` in `workspace`.\n\nRun CustoPiZer. A new file `output.img` will be generated that only differs from the input in having seen its preinstalled OctoPrint\nversion now updated to the latest release.\n\n### Inspecting an image\n\nCustoPiZer also ships with an interactive `enter_image` script that mounts the image, starts the chrooted qemu, drops you into a bash, and on exit from that\nunmounts and exists again.\n\nThis allows you to inspect an existing image prior or post modification, e.g. to test stuff out interactively. Be sure to always operate on a copy of the image\nas the image *will* be changed by you interacting with it, even if only subtly (e.g. file timestamps).\n\nTo use, you have to slightly modify the docker call:\n\n```\ndocker run -it --rm --privileged -v /path/to/image.img:/image.img ghcr.io/octoprint/custopizer:latest /CustoPiZer/enter_image /image.img\n```\n\n## Running from a GitHub Action\n\nThere's a composite action available that can be used as a step inside a GitHub Action workflow:\n\n``` yaml\n- name: Run CustoPiZer\n  uses: OctoPrint/CustoPiZer@main\n  with:\n    workspace: \"${{ github.workspace }}/build\"\n    scripts:  \"${{ github.workspace }}/scripts\"\n```\n\nIt's also possible to pass on a JSON encoded object with additional environment variables to pass on to the docker call and make usable\ninside the script context, e.g.:\n\n``` yaml\n- name: Run CustoPiZer\n  uses: OctoPrint/CustoPiZer@main\n  with:\n    workspace: \"${{ github.workspace }}/build\"\n    scripts:  \"${{ github.workspace }}/scripts\"\n    environment: '{ \"OCTOPRINT_VERSION\": \"${{ env.OCTOPRINT_VERSION }}\" }'\n```\n\n\u003e 👆 **Heads-up**\n\u003e\n\u003e Make sure to use *double quotes* for the JSON object keys and values, otherwise `jq` will raise a syntax error.\n\u003e \n\u003e This is ok:\n\u003e\n\u003e     environment: '{ \"OCTOPRINT_VERSION\": \"${{ env.OCTOPRINT_VERSION }}\" }'\n\u003e\n\u003e This will fail:\n\u003e\n\u003e     environment: \"{ 'OCTOPRINT_VERSION': '${{ env.OCTOPRINT_VERSION }}' }\"\n\nIf you need to provide a custom `config.local`, e.g. to override filesystem extending/shrinking,\nyou can do that via the `config` parameter:\n\n``` yaml\n- name: Run CustoPiZer\n  uses: OctoPrint/CustoPiZer@main\n  with:\n    workspace: \"${{ github.workspace }}/build\"\n    scripts:  \"${{ github.workspace }}/scripts\"\n    config: \"${{ github.workspace }}/config.local\"\n```\n\nAnd finally, you may also provide the tag or digest to use for CustoPiZer itself - helpful if you want to run dev versions, e.g. those built from the `main` branch:\n\n``` yaml\n- name: Run CustoPiZer\n  uses: OctoPrint/CustoPiZer@main\n  with:\n    workspace: \"${{ github.workspace }}/build\"\n    scripts:  \"${{ github.workspace }}/scripts\"\n    custopizer: \"main\"\n```\n\nFor a complex example usage that also includes repository dispatch, creating releases and attaching assets, take a look at the scripts and workflow of [OctoPrint/OctoPi-UpToDate](https://github.com/OctoPrint/OctoPi-UpToDate).\n\n## Writing customization scripts\n\nTo ensure error handling is taken care of and some tooling is available, all customization scripts should start with these lines:\n\n``` bash\nset -x\nset -e\n\nexport LC_ALL=C\n\nsource /common.sh\ninstall_cleanup_trap\n```\n\nThe order in which scripts will be executed is by alphabetical sorting of the name, so it is strongly recommended to separate multiple steps\ninto scripts prefixed `01-`, `02-`, ... to ensure deterministic execution order.\n\nScripts are run as `root` user inside the image, so if you need to do things as a different user, use `sudo -u \u003cuser\u003e`, e.g. `sudo -u pi`.\n\n`common.sh` contains some helpful tools to streamline some common tasks at build time:\n\n  * `unpack \u003csource folder\u003e \u003ctarget folder\u003e \u003ctarget user\u003e`: Copies files from source to target folder, `chown`ing to user and keeping dates and permissions\n  * `is_installed \u003cpackage\u003e`: Succeeds if the package is already installed\n  * `is_in_apt \u003cpackage\u003e`: Succeeds if the package is available in `apt`\n  * `remove_if_installed \u003cpackages\u003e`: Removes the packages if they are installed (interesting for decluttering)\n  * `systemctl_if_exists \u003csystemctl command...\u003e`: Runs the `systemctl` command if `systemctl` is available\n  * `pause \u003cmessage\u003e`: Display message and wait for enter to be pressed, useful for debugging\n  * `echo_red \u003cmessage\u003e`: Display message in red\n  * `echo_green \u003cmessage\u003e`: Display message in green\n\nAny kind of non-`0` exit code will make the build fail, so make sure to develop your update scripts defensively. If a command might fail without\nthe whole build failing, use `|| true`, e.g. `rm some/file || true`.\n\nNote that CustoPiZer will install a policy during build to ensure no services are started up, e.g. when installing new packages.\n\n## Common tasks for customizing OctoPi\n\n### Updating OctoPrint to the latest release\n\n``` bash\nset -x\nset -e\n\nexport LC_ALL=C\n\nsource /common.sh\ninstall_cleanup_trap\n\nif [ -n \"$OCTOPRINT_VERSION\" ]; then\n    sudo -u pi /home/pi/oprint/bin/pip install -U OctoPrint==$OCTOPRINT_VERSION\nelse\n    sudo -u pi /home/pi/oprint/bin/pip install -U OctoPrint\nfi\n```\n\nNote: This also allows you to specify the OctoPrint version to install by setting the environment variable `OCTOPRINT_VERSION` to our target version,\ne.g.\n\n```\ndocker run --rm --privileged \\\n  -e OCTOPRINT_VERSION=1.6.1 \\\n  -v /path/to/workspace:/CustoPiZer/workspace \\\n  ghcr.io/octoprint/custopizer:latest\n```\n\nor for the GitHub action:\n\n``` yaml\n- name: Run CustoPiZer\n  uses: OctoPrint/CustoPiZer@main\n  with:\n    workspace: \"${{ github.workspace }}/build\"\n    scripts:  \"${{ github.workspace }}/scripts\"\n    environment: '{ \"OCTOPRINT_VERSION\": \"1.6.1\" }'\n```\n\nThis also allows to install prereleases. If this environment variable is not set, the latest available release will be installed.\n\n### Preinstalling additional plugins\n\n``` bash\nset -x\nset -e\n\nexport LC_ALL=C\n\nsource /common.sh\ninstall_cleanup_trap\n\nplugins=(\n    # add quoted URLs for install archives, separated by newlines, e.g.:\n    \"https://github.com/jneilliii/OctoPrint-BedLevelVisualizer/archive/master.zip\"\n    \"https://github.com/FormerLurker/ArcWelderPlugin/archive/master.zip\"\n)\n\nfor plugin in ${plugins[@]}; do\n    echo \"Installing plugin from $plugin into OctoPrint venv...\"\n    sudo -u pi /home/pi/oprint/bin/pip install \"$plugin\"\ndone\n\n```\n\n### Adding additional tooling like `avrdude`\n\n``` bash\nset -x\nset -e\n\nexport LC_ALL=C\n\nsource /common.sh\ninstall_cleanup_trap\n\napt install --yes avrdude\n```\n\n### Customizing OctoPrint's configuration\n\nPut the following script into `workspace/scripts/files/settings/merge-settings.py`:\n\n``` python\n#!/usr/bin/env python3\nimport yaml\nimport sys\n\ndef dict_merge(a, b, leaf_merger=None):\n    \"\"\"\n    Recursively deep-merges two dictionaries.\n\n    Taken from https://www.xormedia.com/recursively-merge-dictionaries-in-python/\n\n    Arguments:\n        a (dict): The dictionary to merge ``b`` into\n        b (dict): The dictionary to merge into ``a``\n        leaf_merger (callable): An optional callable to use to merge leaves (non-dict values)\n\n    Returns:\n        dict: ``b`` deep-merged into ``a``\n    \"\"\"\n\n    from copy import deepcopy\n\n    if a is None:\n        a = dict()\n    if b is None:\n        b = dict()\n\n    if not isinstance(b, dict):\n        return b\n    result = deepcopy(a)\n    for k, v in b.items():\n        if k in result and isinstance(result[k], dict):\n            result[k] = dict_merge(result[k], v, leaf_merger=leaf_merger)\n        else:\n            merged = None\n            if k in result and callable(leaf_merger):\n                try:\n                    merged = leaf_merger(result[k], v)\n                except ValueError:\n                    # can't be merged by leaf merger\n                    pass\n\n            if merged is None:\n                merged = deepcopy(v)\n\n            result[k] = merged\n    return result\n\ndef merge_config_files(input_file, config_file):\n    with open(input_file, mode=\"r\", encoding=\"utf8\") as f:\n        to_merge = yaml.safe_load(f)\n\n    with open(config_file, mode=\"r\", encoding=\"utf8\") as f:\n        config = yaml.safe_load(f)\n    \n    merged = dict_merge(config, to_merge)\n\n    with open(config_file, mode=\"w\", encoding=\"utf8\") as f:\n        yaml.safe_dump(merged, f)\n\nif __name__ == \"__main__\":\n    if len(sys.argv) \u003c 2:\n        print(\"usage: merge-settings.py \u003cinput file\u003e \u003ctarget file\u003e\")\n        sys.exit(-1)\n\n    input_file = sys.argv[1]\n    target_file = sys.argv[2]\n\n    print(f\"Merging {input_file} on {target_file}...\")\n    merge_config_files(input_file, target_file)\n    print(f\"Done!\")\n```\n\nPlace a yaml file containing the settings you wish to merge into OctoPrint's active `config.yaml` into `workspace/scripts/files/settings/settings.yaml`.\n\nThen use this customization script:\n\n``` bash\nset -x\nset -e\n\nexport LC_ALL=C\n\nsource /common.sh\ninstall_cleanup_trap\n\n# update config.yaml\nsudo -u pi /home/pi/oprint/bin/python /files/settings/merge-settings.py /files/settings/settings.yaml /home/pi/.octoprint/config.yaml\n```\n\n\u003e ✋ **Warning**\n\u003e\n\u003e Make sure to not ship any secret keys, passphrases, generated UUIDs or similar here. They will otherwise be the same across all instances created with\n\u003e this image!\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foctoprint%2Fcustopizer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foctoprint%2Fcustopizer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foctoprint%2Fcustopizer/lists"}