Skip to content
Snippets Groups Projects
  • Simon McVittie's avatar
    e69ce85e
    locale-gen: Add --output-dir option · e69ce85e
    Simon McVittie authored
    
    At the moment it writes to the current working directory, and we invoke
    it via `bwrap ... --chdir DIRECTORY`; but we want to make it possible to
    use this code from inside a Flatpak runtime (at least in the case where
    we're using the host system's glibc), which means we won't be allowed
    to run bwrap and must do this some other way.
    
    Rather than using `env --chdir=...` (which isn't supported in scout
    anyway), let's teach this script to output to an explicitly specified
    directory.
    
    Signed-off-by: default avatarSimon McVittie <smcv@collabora.com>
    e69ce85e
    History
    locale-gen: Add --output-dir option
    Simon McVittie authored
    
    At the moment it writes to the current working directory, and we invoke
    it via `bwrap ... --chdir DIRECTORY`; but we want to make it possible to
    use this code from inside a Flatpak runtime (at least in the case where
    we're using the host system's glibc), which means we won't be allowed
    to run bwrap and must do this some other way.
    
    Rather than using `env --chdir=...` (which isn't supported in scout
    anyway), let's teach this script to output to an explicitly specified
    directory.
    
    Signed-off-by: default avatarSimon McVittie <smcv@collabora.com>
pressure-vessel-locale-gen 9.00 KiB
#!/bin/sh

# Copyright © 2019 Collabora Ltd.
#
# SPDX-License-Identifier: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

set -e
set -u

me="$0"
me="$(readlink -f "$0")"
here="${me%/*}"
me="${me##*/}"

opt_output_dir=.
opt_verbose=

# Look for try-setlocale helper executable next to this script
PATH="${here}:${PATH}"

LOCALE_ALIAS="/usr/share/locale/locale.alias"

if ! [ -f "$LOCALE_ALIAS" ]; then
    LOCALE_ALIAS=
fi

# We currently only look for I18NDIR data in /usr/share/i18n, the
# glibc default path.
#
# If we discover that some distros use a different default, then we
# should enhance this script to iterate through a search path.
#
# Please keep this in sync with srt_system_info_get_locale_issues().
SUPPORTED="/usr/share/i18n/SUPPORTED"

log () {
    printf '%s\n' "$me: $*" >&2
}

verbose () {
    if [ -n "$opt_verbose" ]; then
        log "$@"
    fi
}

# Return nonzero if the given locale cannot be generated normally.
can_generate () {
    local locale="$1"

    case "$locale" in
        (C|C.UTF-8|C.utf8|POSIX|"")
            return 1
            ;;

        (*..*|*/*)
            log "Avoiding potential path traversal in '$locale'"
            return_ex_config=1
            return 1
            ;;

        (*)
            return 0
            ;;
    esac
}

generate_locale () {
    local at_modifier=
    local codeset=
    local language=
    local list=
    local locale="$1"
    local modifier=
    local supported_codeset=
    local territory=
    local underscore_territory=
    local without_codeset=
    local status=

    language="$locale"

    case "$language" in
        (*@*)
            modifier="${language##*@}"
            at_modifier="@${modifier}"
            language="${language%@*}"
            ;;
    esac

    case "$language" in
        (*.*)
            codeset="${language##*.}"
            language="${language%.*}"
            ;;
    esac

    case "$language" in
        (*_*)
            territory="${language##*_}"
            underscore_territory="_${territory}"
            language="${language%_*}"
            ;;
    esac

    # Speed up locale generation a bit by making e.g. en_GB.utf8 a
    # symlink to en_GB.UTF-8
    if [ "${codeset}" = 'utf8' ]; then
        codeset="UTF-8"
        locale="${language}${underscore_territory}.UTF-8${at_modifier}"
        ln -fns "$locale" \
            "${opt_output_dir}/${language}${underscore_territory}.utf8${at_modifier}"

        if [ -d "$opt_output_dir/$locale" ]; then
            return 0
        fi
    fi

    if [ -n "$codeset" ]; then
        # We don't really need to parse $SUPPORTED to find out that
        # en_US.UTF-8 is a UTF-8 locale. Do nothing. In particular,
        # this works on distributions like Arch Linux that do not
        # normally install $SUPPORTED.
        :
    else
        for list in $SUPPORTED; do
            if supported_codeset="$(
                while read -r supported codeset; do
                    case "$supported" in
                        (\#*)
                            continue
                            ;;
                    esac

                    if [ "$supported" = "$locale" ]; then
                        printf '%s' "$codeset"
                    fi
                done < "$list"
            )"; then
                if [ -n "$supported_codeset" ]; then
                    break
                fi
            else
                supported_codeset=
                verbose "Unable to read $list"
            fi
        done

        if [ -n "$supported_codeset" ]; then
            codeset="$supported_codeset"
        else
            log "Unable to find $locale in $SUPPORTED"
            log "Assuming UTF-8 and hoping that works..."
            codeset="UTF-8"
        fi
    fi

    without_codeset="${language}${underscore_territory}${at_modifier}"

    if localedef \
        ${LOCALE_ALIAS+-A "${LOCALE_ALIAS}"} \
        --no-archive \
        -c \
        -f "$codeset" \
        -i "$without_codeset" \
        "${opt_output_dir}/${locale}" \
    ; then
        log "Generated locale $locale successfully"
    else
        status="$?"

        if [ "$status" = 1 ]; then
            log "Generated locale $locale, with warnings"
        else
            log "Unable to generate locale $locale: $status"
        fi
    fi

    if ! LOCPATH="${opt_output_dir}" pressure-vessel-try-setlocale "$locale"; then
        log "Warning: $locale was generated but does not appear to work!"
    fi
}

usage () {
    local code="$1"
    shift

    if [ "$code" -ne 0 ]; then
        exec >&2
    fi

    echo "Usage: $me [OPTIONS] [LOCALE...]"
    echo
    echo "Generate the locales required by the locale environment"
    echo "variables, and any extra locales from the command line,"
    echo "in the current working directory."
    echo
    echo "Options:"
    echo "--output-dir DIR, -o DIR"
    echo "              Create locales in the given directory [default: .]"
    echo "--verbose     Be more verbose"
    echo
    echo "Arguments:"
    echo "LOCALE        Zero or more locales in POSIX format,"
    echo "              such as en_US.UTF-8, be_BY@latin"
    exit "$code"
}

getopt_temp="help"
getopt_temp="${getopt_temp},output-dir:,output-directory:"
getopt_temp="${getopt_temp},verbose"

getopt_temp="$(getopt -o 'o:' --long "$getopt_temp" -n "$me" -- "$@")"
eval "set -- $getopt_temp"
unset getopt_temp

while [ "$#" -gt 0 ]; do
    case "$1" in
        (-o | --output-dir | --output-directory)
            opt_output_dir="$2"
            shift 2
            ;;

        (--help)
            usage 0
            # not reached
            ;;

        (--verbose)
            opt_verbose=yes
            shift
            ;;

        (--)
            shift
            break
            ;;

        (-*)
            log "Unknown option: $1"
            usage 64    # EX_USAGE from sysexits.h
            # not reached
            ;;

        (*)
            break
            ;;
    esac
done

one_missing=

for var in \
    LC_ADDRESS \
    LC_CTYPE \
    LC_COLLATE \
    LC_IDENTIFICATION \
    LC_MEASUREMENT \
    LC_MESSAGES \
    LC_MONETARY \
    LC_NUMERIC \
    LC_NAME \
    LC_PAPER \
    LC_TELEPHONE \
    LC_TIME \
    \
    HOST_LC_ALL \
    LANG \
    LC_ALL \
; do
    locale="$(eval "printf '%s' \${${var}-}")"

    if [ -z "$locale" ] || ! can_generate "$locale"; then
        continue
    fi

    if ! pressure-vessel-try-setlocale "$locale"; then
        one_missing=yes
        break
    fi
done

for locale in en_US.UTF-8 "$@"; do
    if ! can_generate "$locale" ; then
        continue
    fi

    if ! pressure-vessel-try-setlocale "$locale"; then
        verbose "Missing locale $locale"
        one_missing=yes
        break
    fi
done

if [ -z "$one_missing" ]; then
    verbose "No locales need to be generated"
    exit 0
fi

# We have to generate all the locales we want, not just the ones that
# were missing, because they might have been in a locale archive,
# and setting LOCPATH disables use of the locale archive.

return_ex_cantcreat=
return_ex_config=

for var in \
    LC_ADDRESS \
    LC_CTYPE \
    LC_COLLATE \
    LC_IDENTIFICATION \
    LC_MEASUREMENT \
    LC_MESSAGES \
    LC_MONETARY \
    LC_NUMERIC \
    LC_NAME \
    LC_PAPER \
    LC_TELEPHONE \
    LC_TIME \
    \
    HOST_LC_ALL \
    LANG \
    LC_ALL \
; do
    locale="$(eval "printf '%s' \${${var}-}")"

    if [ -z "$locale" ] || ! can_generate "$locale" ; then
        continue
    fi

    if ! [ -d "$locale" ]; then
        verbose "Generating locale $locale..."
        generate_locale "$locale" || return_ex_cantcreat=yes
    fi
done

for locale in en_US.UTF-8 "$@"; do
    if ! can_generate "$locale" ; then
        continue
    fi

    if ! [ -d "$locale" ]; then
        verbose "Generating locale $locale..."
        generate_locale "$locale" || return_ex_cantcreat=yes
    fi
done

if [ -n "$return_ex_config" ]; then
    exit 78     # EX_CONFIG, "configuration error", from sysexits.h
elif [ -n "$return_ex_cantcreat" ]; then
    exit 73     # EX_CANTCREATE, "can't create (user) output file"
else
    exit 72     # EX_OSFILE, "OS file missing"
fi

# vim:set sw=4 sts=4 et: