From b1a7927d72a4b5db6c122f76fa825b403404a933 Mon Sep 17 00:00:00 2001
From: Simon McVittie <smcv@collabora.com>
Date: Wed, 11 Sep 2019 19:50:09 +0100
Subject: [PATCH] wrap: Use a new pressure-vessel-locale-gen to set up locales

This avoids relying on the host system (if using the host system) or
the container (otherwise) having both its configured locales and
the frequently-hard-coded en_US.UTF-8.

Signed-off-by: Simon McVittie <smcv@collabora.com>
---
 build-relocatable-install.py |   4 +-
 ci/Dockerfile.in             |   2 +
 debian/.gitignore            |   1 +
 debian/control               |   2 +
 debian/copyright             |   6 +
 debian/gitlab-ci.yml         |   2 +
 debian/rules                 |   7 +
 man/locale-gen.1.md          |  86 +++++++++
 man/try-setlocale.1.md       |  60 ++++++
 meson.build                  |  50 ++++-
 meson_options.txt            |   1 +
 pressure-vessel-locale-gen   | 364 +++++++++++++++++++++++++++++++++++
 src/try-setlocale.c          | 114 +++++++++++
 src/wrap.c                   | 147 ++++++++++++++
 tests/shellcheck.sh          |   2 +-
 tests/test-locale-gen.sh     | 236 +++++++++++++++++++++++
 16 files changed, 1081 insertions(+), 3 deletions(-)
 create mode 100644 man/locale-gen.1.md
 create mode 100644 man/try-setlocale.1.md
 create mode 100755 pressure-vessel-locale-gen
 create mode 100644 src/try-setlocale.c
 create mode 100755 tests/test-locale-gen.sh

diff --git a/build-relocatable-install.py b/build-relocatable-install.py
index 507bed216..cf86be7c5 100755
--- a/build-relocatable-install.py
+++ b/build-relocatable-install.py
@@ -96,13 +96,15 @@ PRIMARY_ARCH_DEPENDENCIES = {
     'libselinux1': 'libselinux',
 }
 SCRIPTS = [
+    'pressure-vessel-locale-gen',
     'pressure-vessel-test-ui',
     'pressure-vessel-unruntime',
     'pressure-vessel-unruntime-scout',
     'pressure-vessel-unruntime-test-ui',
 ]
 EXECUTABLES = [
-    'pressure-vessel-wrap'
+    'pressure-vessel-try-setlocale',
+    'pressure-vessel-wrap',
 ]
 LIBCAPSULE_TOOLS = [
     'capsule-capture-libs',
diff --git a/ci/Dockerfile.in b/ci/Dockerfile.in
index e6c775f25..86e6adba2 100644
--- a/ci/Dockerfile.in
+++ b/ci/Dockerfile.in
@@ -18,6 +18,8 @@ apt-get -y install \
     libpcre3 \
     libselinux1 \
     libxau6 \
+    locales \
+    pandoc \
     zlib1g:amd64 \
     zlib1g:i386 \
     ${NULL+}
diff --git a/debian/.gitignore b/debian/.gitignore
index 6a9f93f39..c3b9c3f16 100644
--- a/debian/.gitignore
+++ b/debian/.gitignore
@@ -3,6 +3,7 @@
 /*.log
 /*.substvars
 /files
+/locales/
 /pressure-vessel-libs-*/
 /pressure-vessel-relocatable/
 /tmp/
diff --git a/debian/control b/debian/control
index 6bab4066e..8d88b32de 100644
--- a/debian/control
+++ b/debian/control
@@ -7,7 +7,9 @@ Build-Depends:
  debhelper (>= 9),
  libglib2.0-dev,
  libxau-dev,
+ locales <!nocheck> | locales-all <!nocheck>,
  meson,
+ pandoc,
  python3,
  python3 (>= 3.5) | python3.5,
  zlib1g <!nocheck>,
diff --git a/debian/copyright b/debian/copyright
index 126f0c7fc..d50b123c5 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -18,6 +18,12 @@ Copyright:
  2017-2019 Collabora Ltd.
 License: LGPL-2.1+
 
+Files:
+ src/try-setlocale.*
+Copyright:
+ 2019 Collabora Ltd.
+License: Expat
+
 Files:
  build-aux/git-version-gen
 Copyright:
diff --git a/debian/gitlab-ci.yml b/debian/gitlab-ci.yml
index 3c667e62d..73e724459 100644
--- a/debian/gitlab-ci.yml
+++ b/debian/gitlab-ci.yml
@@ -12,7 +12,9 @@ variables:
         debhelper
         libglib2.0-dev
         libxau-dev
+        locales
         meson
+        pandoc
         python3
         python3.5
         zlib1g
diff --git a/debian/rules b/debian/rules
index 8c670bcab..b7ff2608c 100755
--- a/debian/rules
+++ b/debian/rules
@@ -16,6 +16,7 @@ include /usr/share/dpkg/default.mk
 
 override_dh_auto_clean:
 	rm -fr _build
+	rm -fr debian/locales
 
 override_dh_auto_configure:
 	if python3 -c 'if __import__("sys").version_info < (3, 5): raise SystemExit(1)'; then \
@@ -25,6 +26,7 @@ override_dh_auto_configure:
 	fi; \
 	meson _build \
 		--prefix=/usr/lib/pressure-vessel/relocatable \
+		-Dman=true \
 		-Dpython=/usr/bin/$$python \
 		-Dsrcdir=src \
 		-Dversion=$(DEB_VERSION) \
@@ -34,7 +36,12 @@ override_dh_auto_build:
 	ninja -C _build
 
 override_dh_auto_test:
+ifeq ($(filter nocheck,$(DEB_BUILD_OPTIONS)),)
+	mkdir debian/locales
+	localedef -f UTF-8 -i en_US --no-archive debian/locales/en_US.UTF-8
+	env LOCPATH=$(CURDIR)/debian/locales \
 	meson test -C _build --verbose
+endif
 
 override_dh_auto_install:
 	DESTDIR=$(CURDIR)/debian/tmp ninja -C _build install
diff --git a/man/locale-gen.1.md b/man/locale-gen.1.md
new file mode 100644
index 000000000..b8f500c36
--- /dev/null
+++ b/man/locale-gen.1.md
@@ -0,0 +1,86 @@
+---
+title: pressure-vessel-locale-gen
+section: 1
+...
+
+# NAME
+
+pressure-vessel-locale-gen - generate additional locales
+
+# SYNOPSIS
+
+**pressure-vessel-locale-gen** [*LOCALE*…]
+
+# DESCRIPTION
+
+This tool generates locale files for the locales that might be necessary
+for Steam games. It should be invoked with the current working directory
+set to an empty directory.
+
+If the current working directory is non-empty, the behaviour is undefined.
+Existing subdirectories corresponding to locales might be overwritten, or
+might be kept. (The current implementation is that they are kept, even if
+they are outdated or invalid.)
+
+# OPTIONS
+
+**--verbose**
+:   Be more verbose.
+
+# POSITIONAL ARGUMENTS
+
+*LOCALE*
+:   One or more additional POSIX locale names, such as **en_US.UTF-8** or
+    **it_IT@euro**. By default, **pressure-vessel-locale-gen**
+    generates all the locales required by the standard locale environment
+    variables such as **LC_ALL**, plus the value of *$HOST_LC_ALL* if
+    set, plus the American English locale **en_US.UTF-8** (which is
+    frequently assumed to be present, even though many operating systems
+    do not guarantee it).
+
+# OUTPUT
+
+The diagnostic output on standard error is not machine-readable.
+
+Locale files are generated in the current working directory. On exit,
+if the current working directory is non-empty (exit status 72 or 73),
+its path should be added to the **LOCPATH** environment variable. If
+the current working directory is empty (exit status 0),
+**LOCPATH** should not be modified.
+
+# EXIT STATUS
+
+0
+:   All necessary locales were already available without altering
+    the **LOCPATH** environment variable.
+
+64 (**EX_USAGE** from **sysexits.h**)
+:   Invalid arguments were given.
+
+72 (**EX_OSFILE**)
+:   Not all of the necessary locales were already available, but all
+    were generated in the current working directory.
+
+73 (**EX_CANTCREAT**)
+:   At least one locale was not generated successfully. Some other
+    locales might have been generated in the current working directory
+    as usual.
+
+78 (**EX_CONFIG**)
+:   One of the locales given was invalid or could lead to path traversal.
+    Other locales might have been generated as usual.
+
+Anything else
+:   There was an internal error.
+
+# EXAMPLE
+
+    $ mkdir locales
+    $ ( cd locales; pressure-vessel-locale-gen )
+    $ if [ $? = 0 ]; then \
+      ./bin/some-game; \
+    else \
+      LOCPATH="$(pwd)/locales" ./bin/some-game; \
+    fi
+
+<!-- vim:set sw=4 sts=4 et: -->
diff --git a/man/try-setlocale.1.md b/man/try-setlocale.1.md
new file mode 100644
index 000000000..08714749e
--- /dev/null
+++ b/man/try-setlocale.1.md
@@ -0,0 +1,60 @@
+---
+title: pressure-vessel-try-setlocale
+section: 1
+...
+
+# NAME
+
+pressure-vessel-try-setlocale - check whether a locale works
+
+# SYNOPSIS
+
+**pressure-vessel-try-setlocale** [*LOCALE*]
+
+# DESCRIPTION
+
+This tool checks whether a locale works. If no locale is specified,
+the standard locale environment variables such as **LANG**, **LC\_ALL**
+and **LC\_MESSAGES** are used.
+
+# OPTIONS
+
+There are no options.
+
+# POSITIONAL ARGUMENTS
+
+*LOCALE*
+:   A POSIX locale name, such as **en_US.UTF-8** or **it_IT@euro**,
+    or an empty string to use the standard locale environment variables
+    (which is the default).
+
+# OUTPUT
+
+On failure, an unstructured diagnostic message is printed on standard
+error.
+
+# EXIT STATUS
+
+0
+:   The given locale is available.
+
+1
+:   The given locale is not available.
+
+2
+:   Invalid arguments were given.
+
+# EXAMPLES
+
+The output quoted here is from a system with no Canadian French locale
+available, correctly configured for some other locale.
+
+    $ pressure-vessel-try-setlocale; echo "exit status $?"
+    exit status 0
+    $ pressure-vessel-try-setlocale fr_CA; echo "exit status $?"
+    setlocale "fr_CA": No such file or directory
+    exit status 1
+    $ pressure-vessel-try-setlocale POSIX; echo "exit status $?"
+    exit status 0
+
+<!-- vim:set sw=4 sts=4 et: -->
diff --git a/meson.build b/meson.build
index 6276537a4..295cae89a 100644
--- a/meson.build
+++ b/meson.build
@@ -38,7 +38,6 @@ if version == 'auto'
   version = git_version_gen.stdout().strip()
 endif
 
-
 warning_cflags = [
     '-Wall',
     '-Wextra',
@@ -107,6 +106,7 @@ supported_warning_cflags = c_compiler.get_supported_arguments(project_warning_cf
 add_project_arguments(supported_warning_cflags, language : 'c')
 
 scripts = [
+  'pressure-vessel-locale-gen',
   'pressure-vessel-test-ui',
   'pressure-vessel-unruntime',
   'pressure-vessel-unruntime-scout',
@@ -118,11 +118,13 @@ tests = [
   'pycodestyle.sh',
   'pyflakes.sh',
   'shellcheck.sh',
+  'test-locale-gen.sh',
 ]
 
 test_env = environment()
 test_env.set('G_TEST_SRCDIR', meson.current_source_dir())
 test_env.set('G_TEST_BUILDDIR', meson.current_build_dir())
+test_env.set('PRESSURE_VESSEL_UNINSTALLED', 'yes')
 
 foreach test_name : tests
   if prove.found()
@@ -185,6 +187,52 @@ executable(
   install_rpath : '${ORIGIN}/../' + get_option('libdir'),
 )
 
+executable(
+  'pressure-vessel-try-setlocale',
+  sources : [
+    'src/try-setlocale.c',
+  ],
+  install : true,
+  install_dir : get_option('bindir'),
+)
+
+if get_option('man')
+  pandoc = find_program('pandoc', required : true)
+
+  if run_command(pandoc, [
+    '-f', 'markdown-smart',
+    '-t', 'man',
+    '/dev/null',
+  ]).returncode() == 0
+    pandoc_markdown_nosmart = 'markdown-smart'
+  else
+    pandoc_markdown_nosmart = 'markdown'
+  endif
+
+  foreach tool : ['locale-gen', 'try-setlocale']
+    custom_target(
+      tool + '.1',
+      build_by_default : true,
+      command : [
+        pandoc,
+        '-s',
+        '-o', '@OUTPUT@',
+        '-f', pandoc_markdown_nosmart,
+        '-t', 'man',
+        '@INPUT@',
+      ],
+      input : join_paths('man', tool + '.1.md'),
+      output : 'pressure-vessel-' + tool + '.1',
+      install : true,
+      install_dir : join_paths(
+        get_option('prefix'),
+        get_option('mandir'),
+        'man1',
+      ),
+    )
+  endforeach
+endif
+
 if get_option('srcdir') != ''
   conf_data = configuration_data()
   conf_data.set('prefix', get_option('prefix'))
diff --git a/meson_options.txt b/meson_options.txt
index fe6a3e81e..0931f78eb 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,3 +1,4 @@
+option('man', type : 'boolean', value : false, description : 'enable man pages')
 option('python', type : 'string', value : 'python3')
 option('srcdir', type : 'string', value : '')
 option('version', type : 'string', value : 'auto')
diff --git a/pressure-vessel-locale-gen b/pressure-vessel-locale-gen
new file mode 100755
index 000000000..b9f660e46
--- /dev/null
+++ b/pressure-vessel-locale-gen
@@ -0,0 +1,364 @@
+#!/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_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" "${language}${underscore_territory}.utf8${at_modifier}"
+
+        if [ -d "$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" \
+        ./"${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="$(pwd)" 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 "--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},verbose"
+
+getopt_temp="$(getopt -o '' --long "$getopt_temp" -n "$me" -- "$@")"
+eval "set -- $getopt_temp"
+unset getopt_temp
+
+while [ "$#" -gt 0 ]; do
+    case "$1" in
+        (--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:
diff --git a/src/try-setlocale.c b/src/try-setlocale.c
new file mode 100644
index 000000000..3077ab9a1
--- /dev/null
+++ b/src/try-setlocale.c
@@ -0,0 +1,114 @@
+/*
+ * 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.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <getopt.h>
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Please do not add non-glibc dependencies here: this tool is designed
+ * to work outside the Steam Runtime environment. */
+
+enum
+{
+  OPTION_HELP = 1
+};
+
+struct option long_options[] =
+{
+    { "help", no_argument, NULL, OPTION_HELP },
+    { NULL, 0, NULL, 0 }
+};
+
+static void usage (int code) __attribute__((__noreturn__));
+
+/*
+ * Print usage information and exit with status @code.
+ */
+static void
+usage (int code)
+{
+  FILE *fp;
+
+  if (code == 0)
+    fp = stdout;
+  else
+    fp = stderr;
+
+  fprintf (fp, "Usage: %s [LOCALE]\n",
+           program_invocation_short_name);
+  exit (code);
+}
+
+int
+main (int argc,
+      char **argv)
+{
+  const char *locale_name;
+  const char *locale_result;
+  int opt;
+
+  while ((opt = getopt_long (argc, argv, "", long_options, NULL)) != -1)
+    {
+      switch (opt)
+        {
+          case OPTION_HELP:
+            usage (0);
+            break;
+
+          case '?':
+          default:
+            usage (2);
+            break;  /* not reached */
+        }
+    }
+
+  if (argc < optind || argc > optind + 1)
+    {
+      usage (2);
+    }
+
+  if (argc > optind)
+    locale_name = argv[optind];
+  else
+    locale_name = "";
+
+  locale_result = setlocale (LC_ALL, locale_name);
+
+  if (locale_result == NULL)
+    {
+      int saved_errno = errno;
+
+      fprintf (stderr, "setlocale \"%s\": %s\n",
+               locale_name, strerror (saved_errno));
+      return 1;
+    }
+
+  return 0;
+}
diff --git a/src/wrap.c b/src/wrap.c
index 109d31faf..2a49d77ed 100644
--- a/src/wrap.c
+++ b/src/wrap.c
@@ -31,6 +31,7 @@
 
 #include <locale.h>
 #include <stdlib.h>
+#include <sysexits.h>
 
 #include "libglnx.h"
 
@@ -77,6 +78,9 @@ static void
 search_path_append (GString *search_path,
                     const gchar *item)
 {
+  if (item == NULL || item[0] == '\0')
+    return;
+
   if (search_path->len != 0)
     g_string_append (search_path, ":");
 
@@ -333,6 +337,118 @@ try_bind_dri (FlatpakBwrap *bwrap,
   return TRUE;
 }
 
+/*
+ * Try to make sure we have all the locales we need, by running
+ * the helper from steam-runtime-tools in the container. If this
+ * fails, it isn't fatal - carry on anyway.
+ *
+ * @bwrap must be set up to have the same libc that we will be using
+ * for the container.
+ */
+static void
+ensure_locales (gboolean on_host,
+                const char *tools_dir,
+                FlatpakBwrap *bwrap,
+                const char *overrides)
+{
+  g_autoptr(GError) local_error = NULL;
+  g_autoptr(FlatpakBwrap) run_locale_gen = NULL;
+  g_autofree gchar *locale_gen = NULL;
+  g_autofree gchar *locales = g_build_filename (overrides, "locales", NULL);
+  g_autoptr(GDir) dir = NULL;
+  int exit_status;
+
+  /* bwrap can't own any fds yet, because if it did,
+   * flatpak_bwrap_append_bwrap() would steal them. */
+  g_return_if_fail (bwrap->fds == NULL || bwrap->fds->len == 0);
+
+  g_mkdir (locales, 0700);
+
+  run_locale_gen = flatpak_bwrap_new (NULL);
+
+  if (on_host)
+    {
+      locale_gen = g_build_filename (tools_dir,
+                                     "pressure-vessel-locale-gen",
+                                     NULL);
+
+      flatpak_bwrap_add_args (run_locale_gen,
+                              bwrap->argv->pdata[0],
+                              "--ro-bind", "/", "/",
+                              NULL);
+      pv_bwrap_add_api_filesystems (run_locale_gen);
+      flatpak_bwrap_add_args (run_locale_gen,
+                              "--bind", locales, locales,
+                              "--chdir", locales,
+                              locale_gen,
+                              "--verbose",
+                              NULL);
+    }
+  else
+    {
+      locale_gen = g_build_filename ("/run/host/tools",
+                                     "pressure-vessel-locale-gen",
+                                     NULL);
+
+      flatpak_bwrap_append_bwrap (run_locale_gen, bwrap);
+      pv_bwrap_copy_tree (run_locale_gen, overrides, "/overrides");
+
+      if (!flatpak_bwrap_bundle_args (run_locale_gen, 1, -1, FALSE,
+                                      &local_error))
+        {
+          g_warning ("Unable to set up locale-gen command: %s",
+                     local_error->message);
+          g_clear_error (&local_error);
+        }
+
+      flatpak_bwrap_add_args (run_locale_gen,
+                              "--ro-bind", tools_dir, "/run/host/tools",
+                              "--bind", locales, "/overrides/locales",
+                              "--chdir", "/overrides/locales",
+                              locale_gen,
+                              "--verbose",
+                              NULL);
+    }
+
+  flatpak_bwrap_finish (run_locale_gen);
+
+  /* locale-gen exits 72 (EX_OSFILE) if it had to correct for
+   * missing locales at OS level. This is not an error. */
+  if (!pv_bwrap_run_sync (run_locale_gen, &exit_status, &local_error))
+    {
+      if (exit_status == EX_OSFILE)
+        g_debug ("pressure-vessel-locale-gen created missing locales");
+      else
+        g_warning ("Unable to generate locales: %s", local_error->message);
+
+      g_clear_error (&local_error);
+    }
+  else
+    {
+      g_debug ("No locales generated");
+    }
+
+  dir = g_dir_open (locales, 0, NULL);
+
+  /* If the directory is not empty, make it the container's LOCPATH */
+  if (dir != NULL && g_dir_read_name (dir) != NULL)
+    {
+      g_autoptr(GString) locpath = NULL;
+
+      g_debug ("%s is non-empty", locales);
+
+      locpath = g_string_new ("/overrides/locales");
+      search_path_append (locpath, g_getenv ("LOCPATH"));
+      flatpak_bwrap_add_args (bwrap,
+                              "--setenv", "LOCPATH", locpath->str,
+                              NULL);
+    }
+  else
+    {
+      g_debug ("%s is empty", locales);
+    }
+}
+
 static gboolean
 bind_runtime (FlatpakBwrap *bwrap,
               const char *tools_dir,
@@ -370,6 +486,7 @@ bind_runtime (FlatpakBwrap *bwrap,
   gboolean any_architecture_works = FALSE;
   gboolean any_libc_from_host = FALSE;
   gboolean all_libc_from_host = TRUE;
+  g_autofree gchar *localedef = NULL;
 
   g_return_val_if_fail (tools_dir != NULL, FALSE);
   g_return_val_if_fail (runtime != NULL, FALSE);
@@ -707,6 +824,23 @@ bind_runtime (FlatpakBwrap *bwrap,
                                 "--ro-bind", "/usr/share/i18n",
                                 "/usr/share/i18n",
                                 NULL);
+
+      localedef = g_find_program_in_path ("localedef");
+
+      if (localedef == NULL)
+        {
+          g_warning ("Cannot find localedef in PATH");
+        }
+      else
+        {
+          g_autofree gchar *target = g_build_filename ("/run/host",
+                                                       localedef, NULL);
+
+          flatpak_bwrap_add_args (bwrap,
+                                  "--symlink", target,
+                                      "/overrides/bin/localedef",
+                                  NULL);
+        }
     }
   else
     {
@@ -716,6 +850,8 @@ bind_runtime (FlatpakBwrap *bwrap,
   if (!pv_bwrap_bind_usr (bwrap, "/", "/run/host", error))
     return FALSE;
 
+  ensure_locales (any_libc_from_host, tools_dir, bwrap, overrides);
+
   if (dri_path->len != 0)
       flatpak_bwrap_add_args (bwrap,
                               "--setenv", "LIBGL_DRIVERS_PATH", dri_path->str,
@@ -1248,8 +1384,10 @@ main (int argc,
   g_autofree gchar *tmpdir = NULL;
   g_autofree gchar *scratch = NULL;
   g_autofree gchar *overrides = NULL;
+  g_autofree gchar *overrides_bin = NULL;
   g_autofree gchar *bwrap_executable = NULL;
   g_autoptr(GString) ld_library_path = g_string_new ("");
+  g_autoptr(GString) bin_path = g_string_new ("");
   g_autoptr(GString) adjusted_ld_preload = g_string_new ("");
   g_autofree gchar *cwd_p = NULL;
   g_autofree gchar *cwd_l = NULL;
@@ -1595,6 +1733,8 @@ main (int argc,
   g_mkdir (scratch, 0700);
   overrides = g_build_filename (tmpdir, "overrides", NULL);
   g_mkdir (overrides, 0700);
+  overrides_bin = g_build_filename (overrides, "bin", NULL);
+  g_mkdir (overrides_bin, 0700);
 
   bwrap = flatpak_bwrap_new (NULL);
   flatpak_bwrap_add_arg (bwrap, bwrap_executable);
@@ -1611,6 +1751,13 @@ main (int argc,
     {
       g_debug ("Configuring runtime...");
 
+      search_path_append (bin_path, "/overrides/bin");
+      search_path_append (bin_path, g_getenv ("PATH"));
+      flatpak_bwrap_add_args (bwrap,
+                              "--setenv", "PATH",
+                              bin_path->str,
+                              NULL);
+
       /* TODO: Adapt the use_ld_so_cache code from Flatpak instead
        * of setting LD_LIBRARY_PATH, for better robustness against
        * games that set their own LD_LIBRARY_PATH ignoring what they
diff --git a/tests/shellcheck.sh b/tests/shellcheck.sh
index b8336ec44..85dbaff91 100755
--- a/tests/shellcheck.sh
+++ b/tests/shellcheck.sh
@@ -36,10 +36,10 @@ cd "$G_TEST_SRCDIR"
 
 n=0
 for shell_script in \
-        *.sh \
         pressure-vessel-unruntime \
         pressure-vessel-unruntime-scout \
         pressure-vessel-unruntime-test-ui \
+        pressure-vessel-locale-gen \
         tests/*.sh \
         ; do
     n=$((n + 1))
diff --git a/tests/test-locale-gen.sh b/tests/test-locale-gen.sh
new file mode 100755
index 000000000..3e44f5758
--- /dev/null
+++ b/tests/test-locale-gen.sh
@@ -0,0 +1,236 @@
+#!/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##*/}"
+
+# TAP test helper functions inspired by Perl Test::More.
+
+test_num=0
+any_failed=
+
+skip_all () {
+    echo "1..0 # SKIP $*"
+    exit 0
+}
+
+diag () {
+    echo "# $*"
+}
+
+ok () {
+    test_num=$(( test_num + 1 ))
+    echo "ok $test_num - $*"
+}
+
+skip () {
+    test_num=$(( test_num + 1 ))
+    echo "ok $test_num # SKIP $*"
+}
+
+not_ok () {
+    test_num=$(( test_num + 1 ))
+    any_failed=yes
+    echo "not ok $test_num - $*"
+}
+
+done_testing () {
+    echo "1..$test_num"
+    if [ -n "$any_failed" ]; then
+        exit 1
+    fi
+    exit 0
+}
+
+if ! tmpdir="$(mktemp -d)"; then
+    skip_all "Unable to create a temporary directory"
+fi
+
+if [ -z "${G_TEST_SRCDIR-}" ]; then
+    G_TEST_SRCDIR="${here%/*}"
+fi
+
+if [ -z "${G_TEST_BUILDDIR-}" ]; then
+    G_TEST_BUILDDIR="${here%/*}"
+fi
+
+if [ -n "${PRESSURE_VESSEL_UNINSTALLED-}" ]; then
+    PATH="${G_TEST_BUILDDIR}:${G_TEST_SRCDIR}:${PATH}"
+fi
+
+locale_gen="pressure-vessel-locale-gen"
+try_setlocale="pressure-vessel-try-setlocale"
+
+mkdir "$tmpdir/1"
+
+one_missing=
+
+# Locales used in the test below, chosen to include several that you
+# probably don't have all of.
+for locale in \
+    cs_CZ \
+    cy_GB.UTF-8 \
+    en_AU.UTF-8 \
+    en_DK \
+    en_GB.ISO-8859-15 \
+    en_US.UTF-8 \
+    hu_HU.UTF-8 \
+    it_IT@euro \
+    mn_MN \
+    ms_MY \
+    nb_NO \
+    nan_TW@latin \
+    pl_PL \
+    ta_IN \
+    ti_ER \
+; do
+    if "$try_setlocale" "$locale" >/dev/null; then
+        diag "You already have $locale"
+    else
+        diag "You do not already have $locale"
+        one_missing=yes
+        break
+    fi
+done
+
+if [ -z "$one_missing" ]; then
+    skip "You have all the test locales, cannot test"
+else
+    failed=
+
+    # Deliberately setting environment variables in subshell:
+    # shellcheck disable=SC2030
+    (
+        cd "$tmpdir/1"
+        export LC_CTYPE=cy_GB.UTF-8
+        export LC_COLLATE=cs_CZ
+        export LC_IDENTIFICATION=it_IT@euro
+        export LC_MEASUREMENT=mn_MN
+        export LC_MESSAGES=en_GB.ISO-8859-15
+        export LC_MONETARY=ms_MY
+        export LC_NUMERIC=nb_NO
+        export LC_NAME=nan_TW@latin
+        export LC_PAPER=pl_PL
+        export LC_TELEPHONE=ta_IN
+        export LC_TIME=ti_ER
+
+        export HOST_LC_ALL=hu_HU.UTF-8
+        export LANG=en_DK
+        export LC_ALL=en_AU.UTF-8
+        "$locale_gen"
+    ) || failed="$?"
+
+    if [ "$failed" = 72 ]; then
+        ok "ran $locale_gen, got status 72"
+    elif [ -n "$failed" ]; then
+        not_ok "failed to run $locale_gen"
+    else
+        not_ok "$locale_gen did not report missing locales"
+    fi
+
+    for locale in \
+        cs_CZ \
+        cy_GB.UTF-8 \
+        en_AU.UTF-8 \
+        en_DK \
+        en_GB.ISO-8859-15 \
+        en_US.UTF-8 \
+        hu_HU.UTF-8 \
+        it_IT@euro \
+        mn_MN \
+        ms_MY \
+        nb_NO \
+        nan_TW@latin \
+        pl_PL \
+        ta_IN \
+        ti_ER \
+    ; do
+        if [ -e "$tmpdir/1/$locale/LC_IDENTIFICATION" ]; then
+            ok "$tmpdir/1/$locale/LC_IDENTIFICATION exists"
+        else
+            not_ok "$tmpdir/1/$locale/LC_IDENTIFICATION not generated"
+        fi
+    done
+fi
+
+mkdir "$tmpdir/2"
+
+if "$try_setlocale" "en_US.UTF-8" >/dev/null; then
+    failed=
+
+    # Deliberately setting environment variables in subshell:
+    # shellcheck disable=SC2031
+    (
+        cd "$tmpdir/2"
+        unset LC_CTYPE
+        unset LC_COLLATE
+        unset LC_IDENTIFICATION
+        unset LC_MEASUREMENT
+        unset LC_MESSAGES
+        unset LC_MONETARY
+        unset LC_NUMERIC
+        unset LC_NAME
+        unset LC_PAPER
+        unset LC_TELEPHONE
+        unset LC_TIME
+
+        export HOST_LC_ALL=en_US.UTF-8
+        export LANG=en_US.UTF-8
+        export LC_ALL=en_US.UTF-8
+        "$locale_gen"
+    ) || failed=yes
+
+    if [ -n "$failed" ]; then
+        not_ok "failed to run $locale_gen"
+    else
+        ok "ran $locale_gen"
+    fi
+
+    failed=
+
+    for x in "$tmpdir/2"/*; do
+        if [ -e "$x" ]; then
+            not_ok "$locale_gen generated $x but should not have generated anything"
+            failed=yes
+        fi
+    done
+
+    if [ -z "$failed" ]; then
+        ok "$locale_gen did not redundantly generate en_US.UTF-8"
+    fi
+else
+    skip "Cannot test with existing en_US.UTF-8 locale"
+fi
+
+rm -fr "$tmpdir"
+
+done_testing
+
+# vim:set sw=4 sts=4 et:
-- 
GitLab