| #!/bin/sh |
| # |
| # Copyright 2017 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # 'cros_payload' is a tool to manipulate resources for imaging ChromeOS device. |
| # Run 'cros_payload' to get help information. |
| |
| # This tool must be self-contained with minimal dependency. And at least the |
| # 'installation' part must be implemented in shell script instead of Python |
| # because the ChromeOS factory install shim and netboot installer will need to |
| # run installation without python. |
| |
| # External dependencies: |
| # jq curl md5sum partx|cgpt pigz|gzip |
| # dd tee od chmod dirname readlink mktemp stat |
| # cp mv ln rm |
| |
| # TODO(hungte) List of todo: |
| # - Quick check dependency before starting to run. |
| # - Add partitions in parallel if pigz cannot be found. |
| # - Consider using xz/pixz instead of gz. |
| # - Support adding or removing single partition directly. |
| # - Add part0 as GPT itself. |
| |
| # Environment settings for utilities to invoke. |
| : "${GZIP:="gzip"}" |
| : "${JQ:=""}" |
| |
| # Debug settings |
| : "${DEBUG:=}" |
| |
| # Constants |
| COMPONENTS_ALL="test_image release_image toolkit hwid firmware" |
| |
| # A variable for the file name of tracking temp files. |
| TMP_OBJECTS="" |
| |
| # Cleans up any temporary files we have created. |
| # Usage: cleanup |
| cleanup() { |
| trap - EXIT |
| local object |
| if [ -n "${TMP_OBJECTS}" ]; then |
| while read object; do |
| if [ -d "${object}" ]; then |
| umount -d "${object}" 2>/dev/null |
| fi |
| rm -f "${object}" |
| done <"${TMP_OBJECTS}" |
| rm -f "${TMP_OBJECTS}" |
| fi |
| } |
| |
| # Prints error message and try to abort. |
| # Usage: die [messages] |
| die() { |
| trap - EXIT |
| echo "ERROR: $*" >&2 |
| cleanup |
| exit 1 |
| } |
| |
| # Prints information to users. |
| # Usage: info [message] |
| info() { |
| echo "INFO: $*" >&2 |
| } |
| |
| # Prints debug messages if ${DEBUG} was set to non-empty values. |
| # Usage: debug [message] |
| debug() { |
| [ -z "${DEBUG}" ] || echo "DEBUG: $*" >&2 |
| } |
| |
| # Registers a temp object to be deleted on cleanup. |
| # Usage: register_tmp_object TEMP_FILE |
| register_tmp_object() { |
| # Use file-based temp object tracker so execution in sub-shell (command | |
| # while read var) will also work. |
| echo "$*" >>"${TMP_OBJECTS}" |
| } |
| |
| # Checks if given file is already compressed by gzip. |
| # Usage: is_gzipped FILE |
| is_gzipped() { |
| local input="$1" |
| |
| # The 'file' command needs special database, so we want to directly read the |
| # magic value. |
| local magic="$(od -An -N2 -x "${input}")" |
| local gzip_magic=" 8b1f" |
| |
| [ "${magic}" = "${gzip_magic}" ] |
| } |
| |
| # Checks if a tool program is already available on path. |
| # Usage: has_tool PROGRAM |
| has_tool() { |
| type "$1" >/dev/null 2>&1 |
| } |
| |
| # Downloads from given URL. If output is not given, use STDOUT. |
| # Usage: fetch URL [output] |
| fetch() { |
| local url="$1" |
| local output="$2" |
| if [ -n "${output}" ]; then |
| curl -L --fail "${url}" -o "${output}" |
| else |
| curl -L --fail "${url}" |
| fi |
| } |
| |
| # Prints a path for specific partition on given device. |
| # For example, "get_partition_dev /dev/mmcblk0 1" prints /dev/mmcblk0p1. |
| # Usage: get_partition_dev DEV PART_NO |
| get_partition_dev() { |
| local base="$1" |
| shift |
| |
| # TODO(hungte) decide if we should also test -b ${base}. |
| case "${base}" in |
| *[0-9]) |
| # Adjust from /dev/mmcblk0 to /dev/mmcblk0p |
| echo "${base}p$*" |
| ;; |
| "") |
| die "Need valid partition device" |
| ;; |
| *) |
| echo "${base}$*" |
| esac |
| } |
| |
| # Command "help", provides usage help. |
| # Usage: cmd_help |
| cmd_help() { |
| echo "Usage: $0 command [args...] |
| |
| '$(basename "$0")' is an utility for manipulating payload-type resources. |
| All the payloads will be stored as gzipped file with MD5SUM in |
| file name. A JSON file manages the mapping to individual files. |
| |
| Commands: |
| add JSON_PATH COMPONENT FILE |
| install JSON_URL DEST [COMPONENTS...] |
| list JSON_URL |
| |
| COMPONENT: ${COMPONENTS_ALL} |
| JSON_PATH: A path to local JSON config file. |
| JSON_URL: A URL to remote or local JSON config file. |
| FILE: A path to local file resource. |
| DEST: Destination (usually a folder or a block device like /dev/sda). |
| " |
| } |
| |
| # JSON helper functions - using jq or python. |
| |
| # Merges two json arguments into out. |
| json_merge() { |
| local base_path="$1" |
| local update_path="$2" |
| |
| if [ -n "${JQ}" ]; then |
| jq -s '.[0] * .[1]' "${base_path}" "${update_path}" |
| return |
| fi |
| |
| python -c "\ |
| import json |
| import sys |
| |
| def get_fd(path): |
| return sys.stdin if path == '-' else open(path) |
| |
| def merge(base, delta): |
| for key, value in delta.iteritems(): |
| if isinstance(value, dict): |
| new_value = base.setdefault(key, {}) |
| merge(new_value, value) |
| else: |
| base[key] = value |
| return base |
| |
| base = json.load(get_fd(sys.argv[1])) |
| delta = json.load(get_fd(sys.argv[2])) |
| merge(base, delta) |
| print(json.dumps(base)) |
| " "${base_path}" "${update_path}" |
| } |
| |
| # Gets the value of specified query command. |
| json_get_value() { |
| local query="$1" |
| local json_file="$2" |
| if [ -n "${JQ}" ]; then |
| "${JQ}" -r "${query}" "${json_file}" |
| return |
| fi |
| |
| python -c "\ |
| import json |
| import sys |
| |
| j = json.load(open(sys.argv[2])) |
| for k in sys.argv[1].split('.')[1:]: |
| j = j.get(k) if j else None |
| print('null' if j is None else j)" "${query}" "${json_file}" |
| } |
| |
| # Gets the keys of given JSON object from stdin. |
| json_get_keys() { |
| if [ -n "${JQ}" ]; then |
| "${JQ}" -r 'keys[]' |
| return |
| fi |
| |
| python -c "import json; import sys; print('\n'.join(json.load(sys.stdin)))" |
| } |
| |
| # Updates JSON data to specified config. |
| update_json() { |
| local json_path="$1" |
| local new_json="$2" |
| |
| local new_config="$(mktemp)" |
| register_tmp_object "${new_config}" |
| |
| echo "${new_json}" | json_merge "${json_path}" - >"${new_config}" |
| cp -f "${new_config}" "${json_path}" |
| chmod a+r "${json_path}" |
| } |
| |
| # Commits a payload into given location. |
| # Usage: commit_payload COMPONENT SUBTYPE MD5SUM TEMP_PAYLOAD DIR |
| commit_payload() { |
| local component="$1" |
| local subtype="$2" |
| local md5sum="$3" |
| local temp_payload="$4" |
| local dir="$5" |
| local json output_name |
| |
| # Derived variables |
| [ -n "${md5sum}" ] || die "Fail to get MD5 for ${component}.${subtype}." |
| [ -e "${temp_payload}" ] || die "Fail to find temporary file ${temp_payload}." |
| |
| if [ -n "${subtype}" ]; then |
| output_name="${component}_${subtype}_${md5sum}.gz" |
| json="{\"${component}\": {\"${subtype}\": \"${output_name}\"}}" |
| else |
| output_name="${component}_${md5sum}.gz" |
| json="{\"${component}\": \"${output_name}\"}" |
| fi |
| local output="${dir}/${output_name}" |
| |
| # Ideally TEMP_PAYLOAD should be deleted by cleanup, and we do want to prevent |
| # deleting it in order to prevent race condition if multiple instances of this |
| # program is running (so mktemp won't reuse names). |
| rm -f "${output}" |
| ln "${temp_payload}" "${output}" || cp -f "${temp_payload}" "${output}" |
| |
| chmod a+r "${output}" |
| update_json "${json_path}" "${json}" |
| } |
| |
| # Adds an image partition type payload. |
| # Usage: add_image_part JSON_PATH COMPONENT FILE PART_NO START SIZE |
| # FILE is the disk image file. |
| # PART_NO is the number (NR) of partition number. |
| # START is the starting sector (bs=512) of partition. |
| # SIZE is the number of sectors in partition. |
| add_image_part() { |
| local json_path="$1" |
| local component="$2" |
| local file="$3" |
| local nr="$4" |
| local start="$5" |
| local sectors="$6" |
| |
| local md5sum="" |
| local output="" |
| local output_dir="$(dirname "$(readlink -f "${json_path}")")" |
| |
| local tmp_file="$(mktemp -p "${output_dir}" tmp_XXXXXX.gz)" |
| register_tmp_object "${tmp_file}" |
| |
| # TODO(hungte) if nr is 3, fetch lsb-release version. |
| info "Adding component ${component} part ${nr} ($((sectors / 2048))M)..." |
| md5sum="$( |
| dd if="${file}" bs=512 skip="${start}" count="${sectors}" 2>/dev/null | \ |
| "${GZIP}" -qc | tee "${tmp_file}" | md5sum -b)" |
| |
| commit_payload "${component}" "part${nr}" "${md5sum%% *}" \ |
| "${tmp_file}" "${output_dir}" |
| |
| } |
| |
| # Adds an disk image type payload. |
| # Usage: add_image_component JSON_PATH COMPONENT FILE |
| add_image_component() { |
| local json_path="$1" |
| local component="$2" |
| local file="$3" |
| local nr start sectors uuid part_command |
| |
| # TODO(hungte) Support image in compressed form (for example, test image in |
| # tar.xz) using tar -O. |
| if has_tool cgpt; then |
| part_command="cgpt show -q -n" |
| elif has_tool partx; then |
| # The order must be same as what CGPT outputs. |
| part_command="partx -g -r -o START,SECTORS,NR,UUID" |
| else |
| die "Missing partition tools - please install cgpt or partx." |
| fi |
| |
| ${part_command} "${file}" | while read start sectors nr uuid; do |
| debug "${part_command} ${file} -> ${start} ${sectors} ${nr} ${uuid}" |
| # ${uuid} is not really needed for add_image_part. |
| add_image_part "${json_path}" "${component}" "${file}" "${nr}" \ |
| "${start}" "${sectors}" |
| done |
| } |
| |
| # Adds a simple file type payload. |
| # Usage: add_file_component JSON_PATH COMPONENT FILE |
| add_file_component() { |
| local json_path="$1" |
| local component="$2" |
| local file="$3" |
| |
| local md5sum="" |
| local output="" |
| local file_size="$(($(stat -c "%s" "${file}") / 1048576))M" |
| local output_dir="$(dirname "$(readlink -f "${json_path}")")" |
| |
| local tmp_file="$(mktemp -p "${output_dir}" tmp_XXXXXX.gz)" |
| register_tmp_object "${tmp_file}" |
| |
| if is_gzipped "${file}"; then |
| # Simply copy to destination |
| info "Adding component ${component} (${file_size}, gzipped)..." |
| md5sum="$(tee "${tmp_file}" <"${file}" | md5sum -b)" |
| else |
| # gzip and copy at the same time. If we want to prevent storing original |
| # file name, add -n. |
| info "Adding component ${component} (${file_size})..." |
| md5sum="$("${GZIP}" -qc "${file}" | tee "${tmp_file}" | md5sum -b)" |
| fi |
| commit_payload "${component}" "" "${md5sum%% *}" "${tmp_file}" "${output_dir}" |
| } |
| |
| # Command "add", to add a component into payloads. |
| # Usage: cmd_add JSON_PATH COMPONENT FILE |
| cmd_add() { |
| local json_path="$1" |
| local component="$2" |
| local file="$3" |
| |
| if [ ! -e "${json_path}" ]; then |
| die "Invalid JSON config path: ${json_path}" |
| fi |
| if [ ! -e "${file}" ]; then |
| die "Missing input file: ${file}" |
| fi |
| |
| case "${component}" in |
| release_image | test_image) |
| add_image_component "${json_path}" "${component}" "${file}" |
| ;; |
| firmware | hwid | toolkit) |
| add_file_component "${json_path}" "${component}" "${file}" |
| ;; |
| *) |
| die "Unknown component: ${component}" |
| ;; |
| esac |
| } |
| |
| # Installs a disk image partition type payload to given location. |
| # Usage: install_partition JSON_URL DEST JSON_FILE COMPONENT MAPPINGS... |
| install_partition() { |
| local json_url="$1"; shift |
| local dest="$1"; shift |
| local json_file="$1"; shift |
| local component="$1"; shift |
| local json_url_base="$(dirname "${json_url}")" |
| local remote_file="" remote_url="" dest_part_dev="" |
| local mapping part_from part_to |
| |
| # Each mapping comes in "from_NR to_NR" format. |
| # TODO(hungte) Install partitions in parallel if pigz is not available. |
| for mapping in "$@"; do |
| part_from="${mapping% *}" |
| part_to="${mapping#* }" |
| remote_file="$( \ |
| json_get_value ".${component}.part${part_from}" "${json_file}")" |
| if [ "${remote_file}" = "null" ]; then |
| die "Missing payload ${component}.part${part_from} from ${json_url}." |
| fi |
| remote_url="${json_url_base}/${remote_file}" |
| dest_part_dev="$(get_partition_dev "${dest}" "${part_to}")" |
| [ -b "${dest_part_dev}" ] || die "Not a block device: ${dest_part_dev}" |
| info "Installing from ${component}#${part_from} to ${dest_part_dev} ..." |
| # TODO(hungte) Support better dd/pv, pre-fetch size. |
| # bs is fixed on 1048576 because many dd implementations do not support |
| # units like '1M' or '1m'. |
| fetch "${remote_url}" | "${GZIP}" -d | \ |
| dd of="${dest_part_dev}" bs=1048576 iflag=fullblock oflag=dsync |
| done |
| } |
| |
| # Adds a stub file for component installation. |
| # Usage: install_add_stub DIR COMPONENT |
| install_add_stub() { |
| local payloads_dir="$1" |
| local component="$2" |
| local output_dir="${payloads_dir}/install" |
| # Chrome OS test images may disable symlink and +exec on stateful partition, |
| # so we have to implement the stub as pure shell scripts, and invoke the |
| # component via shell. |
| local stub="${output_dir}/${component}.sh" |
| local command="sh ./${component}" |
| |
| case "${component}" in |
| toolkit) |
| command="sh ./${component} --yes" |
| ;; |
| hwid) |
| ;; |
| *) |
| return |
| esac |
| |
| # Decompress now to reduce installer dependency. |
| "${GZIP}" -d "${payloads_dir}/${component}.gz" |
| |
| mkdir -m 0755 -p "${output_dir}" |
| echo '#!/bin/sh' >"${stub}" |
| # shellcheck disable=SC2016 |
| echo 'cd "$(dirname "$(readlink -f "$0")")"/..' >>"${stub}" |
| echo "${command}" >>"${stub}" |
| } |
| |
| # Installs a file type payload to given location. |
| # Usage: install_file JSON_URL DEST COMPONENT |
| install_file() { |
| local json_url="$1"; shift |
| local dest="$1"; shift |
| local json_file="$1"; shift |
| local component="$1"; shift |
| local json_url_base="$(dirname "${json_url}")" |
| local output="" |
| |
| local remote_file="$(json_get_value ".${component}" "${json_file}")" |
| local remote_url="${json_url_base}/${remote_file}" |
| |
| if [ -d "${dest}" ]; then |
| # The destination is a directory. |
| output="${dest}/${component}.gz" |
| echo "Installing from ${component} to ${output} ..." |
| fetch "${remote_url}" "${dest}/${component}.gz" |
| elif [ -b "${dest}" ]; then |
| local dev="$(get_partition_dev "${dest}" 1)" |
| if [ ! -b "${dev}" ]; then |
| # The destination is a block device file for partition. |
| dev="${dest}" |
| fi |
| local mount_point="$(mktemp -d)" |
| register_tmp_object "${mount_point}" |
| mount "${dev}" "${mount_point}" |
| |
| echo "Installing from ${component} to ${dev}!cros_payloads/${component}.gz" |
| local out_dir="${mount_point}/cros_payloads" |
| mkdir -p "${out_dir}" |
| output="${out_dir}/${component}.gz" |
| fetch "${remote_url}" "${output}" |
| install_add_stub "${output}" "${component}" |
| umount "${mount_point}" |
| elif [ "${dest%.gz}" = "${dest}" ]; then |
| # The destination is an uncompressed file. |
| output="${dest}" |
| echo "Installing from ${component} to ${output} ..." |
| fetch "${remote_url}" | "${GZIP}" -d >"${output}" |
| else |
| # The destination is a compressed file. |
| output="${dest}.gz" |
| echo "Installing from ${component} to ${output} ..." |
| fetch "${remote_url}" "${output}" |
| fi |
| } |
| |
| # Prints a curl friendly canonical URL of given argument. |
| # Usage: get_canonical_url URL |
| get_canonical_url() { |
| local url="$*" |
| |
| case "${url}" in |
| *"://"*) |
| echo "$*" |
| ;; |
| "") |
| die "Missing URL." |
| ;; |
| *) |
| echo "file://$(readlink -f "${url}")" |
| ;; |
| esac |
| } |
| |
| # Command "install", to allow installing components to target. |
| # Usage: cmd_install JSON_URL DEST_DEV [COMPONENTS...] |
| # When not specified, try to install all components available. |
| cmd_install() { |
| local json_url="$1"; shift |
| local dest="$1"; shift |
| [ -n "${json_url}" ] || die "Need JSON URL." |
| if [ ! -e "${dest}" ] && [ ! -e "$(dirname "${dest}")" ]; then |
| die "Need existing destination (${dest})." |
| fi |
| local component |
| local components="$*" |
| |
| local json_file="$(mktemp)" |
| register_tmp_object "${json_file}" |
| |
| if [ -z "${components}" ]; then |
| components="${COMPONENTS_ALL}" |
| fi |
| json_url="$(get_canonical_url "${json_url}")" |
| |
| info "Getting JSON config from ${json_url}..." |
| fetch "${json_url}" "${json_file}" |
| |
| # All ChromeOS USB image sources have |
| # 2 = Recovery Kernel |
| # 3 = Root FS |
| # 4 = Normal Kernel |
| # And the installation on fixed storage should be |
| # 2 = Test Image Normal Kernel |
| # 3 = Test Image Root FS |
| # 4 = Release Image Normal Kernel |
| # 5 = Release Image Root FS |
| |
| for component in ${components}; do |
| case "${component}" in |
| test_image) |
| install_partition \ |
| "${json_url}" "${dest}" "${json_file}" "${component}" \ |
| "1 1" "4 2" "3 3" |
| ;; |
| release_image) |
| install_partition \ |
| "${json_url}" "${dest}" "${json_file}" "${component}" \ |
| "4 4" "3 5" \ |
| "6 6" "7 7" "8 8" "9 9" "10 10" "11 11" "12 12" |
| ;; |
| toolkit | hwid | firmware) |
| install_file \ |
| "${json_url}" "${dest}" "${json_file}" "${component}" \ |
| "" |
| ;; |
| *) |
| # TODO(hungte) In future we may be able to install arbitrary component |
| # from JSON file. |
| die "Unknown component: ${component}" |
| esac |
| done |
| |
| # TODO(hungte) Complete installation. |
| } |
| |
| # Lists available components on JSON URL. |
| # Usage: cmd_list "$@" |
| cmd_list() { |
| local json_url="$1" |
| json_url="$(get_canonical_url "${json_url}")" |
| |
| info "Getting JSON config from ${json_url}..." |
| fetch "${json_url}" | json_get_keys |
| } |
| |
| # Main entry. |
| # Usage: main "$@" |
| main() { |
| if [ "$#" -lt 2 ]; then |
| cmd_help |
| exit |
| fi |
| set -e |
| trap "die Execution failed." EXIT |
| |
| if has_tool pigz; then |
| GZIP="pigz" |
| fi |
| if has_tool jq; then |
| JQ="jq" |
| fi |
| umask 022 |
| |
| TMP_OBJECTS="$(mktemp)" |
| case "$1" in |
| add) |
| shift |
| cmd_add "$@" |
| ;; |
| install) |
| shift |
| cmd_install "$@" |
| ;; |
| list) |
| shift |
| cmd_list "$@" |
| ;; |
| *) |
| cmd_help |
| die "Unknown command: $1" |
| ;; |
| esac |
| trap cleanup EXIT |
| } |
| main "$@" |