LoginSignup
1
0

More than 1 year has passed since last update.

pyenv installを覗いてみる

Posted at

概要

pyenvのパスの理解の仕方とインストールの手順が気になったので調べてみた。
前回はこちら

内容

ややはやりぎだが、早めに辿り着く。

/usr/local/opt/pyenv/libexec/pyenv-install

は、ない。

/usr/local/opt/pyenv/bin/pyenv-install

少しずつ見ていく。

set -e
[ -n "$PYENV_DEBUG" ] && set -x

決まり文句。

# Add `share/python-build/` directory from each pyenv plugin to the list of
# paths where build definitions are looked up.
shopt -s nullglob
for plugin_path in "$PYENV_ROOT"/plugins/*/share/python-build; do
  PYTHON_BUILD_DEFINITIONS="${PYTHON_BUILD_DEFINITIONS}:${plugin_path}"
done
export PYTHON_BUILD_DEFINITIONS
shopt -u nullglob

nullglobは空でもループできるようにするためのもの。
ここでは、~/.pyenv/pluginsは存在しないので、あまり気にしない。

# Provide pyenv completions
if [ "$1" = "--complete" ]; then
  echo --list
  echo --force
  echo --skip-existing
  echo --keep
  echo --patch
  echo --verbose
  echo --version
  echo --debug
  exec python-build --definitions
fi

コンプリートオプションがセットされた時の動作。インストール可能なものをリストアップできる。
なおwhich python-build: /usr/local/bin/python-build

# Load shared library functions
eval "$(python-build --lib)"

よくわかっていない。

usage() {
  pyenv-help install 2>/dev/null
  [ -z "$1" ] || exit "$1"
}

ヘルプを表示するためのものだろう。

definitions() {
  local query="$1"
  python-build --definitions | $(type -P ggrep grep | head -1) -F "$query" || true
}

python-build definitionsは指定可能なバージョンを表示するもの。

indent() {
  sed 's/^/  /'
}

インデント。sedを使うのかなるほど。

unset FORCE
unset SKIP_EXISTING
unset KEEP
unset VERBOSE
unset HAS_PATCH
unset DEBUG

[ -n "$PYENV_DEBUG" ] && VERBOSE="-v"

どこかで使われていたのかもしれない。

parse_options "$@"
for option in "${OPTIONS[@]}"; do
  case "$option" in
  "h" | "help" )
    usage 0
    ;;
  "l" | "list" )
    echo "Available versions:"
    definitions | indent
    exit
    ;;
  "f" | "force" )
    FORCE=true
    ;;
  "s" | "skip-existing" )
    SKIP_EXISTING=true
    ;;
  "k" | "keep" )
    [ -n "${PYENV_BUILD_ROOT}" ] || PYENV_BUILD_ROOT="${PYENV_ROOT}/sources"
    ;;
  "v" | "verbose" )
    VERBOSE="-v"
    ;;
  "p" | "patch" )
    HAS_PATCH="-p"
    ;;
  "g" | "debug" )
    DEBUG="-g"
    ;;
  "version" )
    exec python-build --version
    ;;
  * )
    usage 1 >&2
    ;;
  esac
done

オプションの理解。

[ "${#ARGUMENTS[@]}" -le 1 ] || usage 1 >&2

unset VERSION_NAME

# The first argument contains the definition to install. If the
# argument is missing, try to install whatever local app-specific
# version is specified by pyenv. Show usage instructions if a local
# version is not specified.
DEFINITION="${ARGUMENTS[0]}"
[ -n "$DEFINITION" ] || DEFINITION="$(pyenv-local 2>/dev/null || true)"
[ -n "$DEFINITION" ] || usage 1 >&2

VERSIONの指定がされているか、指定されていなければ指定しようと試みている。

# Define `before_install` and `after_install` functions that allow
# plugin hooks to register a string of code for execution before or
# after the installation process.
declare -a before_hooks after_hooks

before_install() {
  local hook="$1"
  before_hooks["${#before_hooks[@]}"]="$hook"
}

after_install() {
  local hook="$1"
  after_hooks["${#after_hooks[@]}"]="$hook"
}
OLDIFS="$IFS"
IFS=$'\n' scripts=(`pyenv-hooks install`)
IFS="$OLDIFS"
for script in "${scripts[@]}"; do source "$script"; done
# Set VERSION_NAME from $DEFINITION, if it is not already set. Then
# compute the installation prefix.
[ -n "$VERSION_NAME" ] || VERSION_NAME="${DEFINITION##*/}"
[ -n "$DEBUG" ] && VERSION_NAME="${VERSION_NAME}-debug"
PREFIX="${PYENV_ROOT}/versions/${VERSION_NAME}"

[ -d "${PREFIX}" ] && PREFIX_EXISTS=1

ここで、PREFIX="${PYENV_ROOT}/versions/${VERSION_NAME}"

# If the installation prefix exists, prompt for confirmation unless
# the --force option was specified.
if [ -d "${PREFIX}/bin" ]; then
  if [ -z "$FORCE" ] && [ -z "$SKIP_EXISTING" ]; then
    echo "pyenv: $PREFIX already exists" >&2
    read -p "continue with installation? (y/N) "

    case "$REPLY" in
    y | Y | yes | YES ) ;;
    * ) exit 1 ;;
    esac
  elif [ -n "$SKIP_EXISTING" ]; then
    # Since we know the python version is already installed, and are opting to
    # not force installation of existing versions, we just `exit 0` here to
    # leave things happy
    exit 0
  fi
fi
# If PYENV_BUILD_ROOT is set, always pass keep options to python-build.
if [ -n "${PYENV_BUILD_ROOT}" ]; then
  export PYTHON_BUILD_BUILD_PATH="${PYENV_BUILD_ROOT}/${VERSION_NAME}"
  KEEP="-k"
fi

ここで、export PYTHON_BUILD_BUILD_PATH="${PYENV_BUILD_ROOT}/${VERSION_NAME}"

# Set PYTHON_BUILD_CACHE_PATH to $PYENV_ROOT/cache, if the directory
# exists and the variable is not already set.
if [ -z "${PYTHON_BUILD_CACHE_PATH}" ] && [ -d "${PYENV_ROOT}/cache" ]; then
  export PYTHON_BUILD_CACHE_PATH="${PYENV_ROOT}/cache"
fi

ここで、export PYTHON_BUILD_CACHE_PATH="${PYENV_ROOT}/cache"

if [ -z "${PYENV_BOOTSTRAP_VERSION}" ]; then
  case "${VERSION_NAME}" in
  [23]"."* )
    # Default PYENV_VERSION to the friendly Python version. (The
    # CPython installer requires an existing Python installation to run. An
    # unsatisfied local .python-version file can cause the installer to
    # fail.)
    for version_info in "${VERSION_NAME%-dev}" "${VERSION_NAME%.*}" "${VERSION_NAME%%.*}"; do
      # Anaconda's `curl` doesn't work on platform where `/etc/pki/tls/certs/ca-bundle.crt` isn't available (e.g. Debian)
      for version in $(pyenv-whence "python${version_info}" 2>/dev/null || true); do
        if [[ "${version}" != "anaconda"* ]] && [[ "${version}" != "miniconda"* ]]; then
          PYENV_BOOTSTRAP_VERSION="${version}"
          break 2
        fi
      done
    done
    ;;
  "pypy"*"-dev" | "pypy"*"-src" )
    # PyPy/PyPy3 requires existing Python 2.7 to build
    if [ -n "${PYENV_RPYTHON_VERSION}" ]; then
      PYENV_BOOTSTRAP_VERSION="${PYENV_RPYTHON_VERSION}"
    else
      for version in $(pyenv-versions --bare | sort -r); do
        if [[ "${version}" == "2.7"* ]]; then
          PYENV_BOOTSTRAP_VERSION="$version"
          break
        fi
      done
    fi
    if [ -n "$PYENV_BOOTSTRAP_VERSION" ]; then
      for dep in curses genc pycparser; do
        if ! PYENV_VERSION="$PYENV_BOOTSTRAP_VERSION" pyenv-exec python -c "import ${dep}" 1>/dev/null 2>&1; then
          echo "pyenv-install: $VERSION_NAME: PyPy requires \`${dep}' in $PYENV_BOOTSTRAP_VERSION to build from source." >&2
          exit 1
        fi
      done
    else
      echo "pyenv-install: $VERSION_NAME: PyPy requires Python 2.7 to build from source." >&2
      exit 1
    fi
    ;;
  esac
fi
if [ -n "${PYENV_BOOTSTRAP_VERSION}" ]; then
  export PYENV_VERSION="${PYENV_BOOTSTRAP_VERSION}"
fi
# Execute `before_install` hooks.
for hook in "${before_hooks[@]}"; do eval "$hook"; done
# Plan cleanup on unsuccessful installation.
cleanup() {
  [ -z "${PREFIX_EXISTS}" ] && rm -rf "$PREFIX"
}

trap cleanup SIGINT
# Invoke `python-build` and record the exit status in $STATUS.
STATUS=0
python-build $KEEP $VERBOSE $HAS_PATCH $DEBUG "$DEFINITION" "$PREFIX" || STATUS="$?"

ここで、インストールが行われる。なので、python-buildを見る。

# Display a more helpful message if the definition wasn't found.
if [ "$STATUS" == "2" ]; then
  { candidates="$(definitions "$DEFINITION")"
    here="$(dirname "${0%/*}")/../.."
    if [ -n "$candidates" ]; then
      echo
      echo "The following versions contain \`$DEFINITION' in the name:"
      echo "$candidates" | indent
    fi
    echo
    echo "See all available versions with \`pyenv install --list'."
    echo
    echo -n "If the version you need is missing, try upgrading pyenv"
    if [ "$here" != "${here#$(brew --prefix 2>/dev/null)}" ]; then
      printf ":\n\n"
      echo "  brew update && brew upgrade pyenv"
    elif [ -d "${here}/.git" ]; then
      printf ":\n\n"
      echo "  cd ${here} && git pull && cd -"
    else
      printf ".\n"
    fi
  } >&2
fi
# Execute `after_install` hooks.
for hook in "${after_hooks[@]}"; do eval "$hook"; done
# Run `pyenv-rehash` after a successful installation.
if [ "$STATUS" == "0" ]; then
  pyenv-rehash
else
  cleanup
fi

exit "$STATUS"

/usr/local/opt/pyenv/bin/python-build

少しずつ見て行こうかと思ったが、2000行あるので、やめておく。
python-build $KEEP $VERBOSE $HAS_PATCH $DEBUG "$DEFINITION" "$PREFIX" || STATUS="$?"に関連するところだけ見る。
なお、これは私の環境では、python-build -v 3.7.0 /Users/yo314159265/.pyenv/versions/3.7.0になる。

PYTHON_BUILD_DEFINITIONS

pyenv-build
PYTHON_BUILD_INSTALL_PREFIX="$(abs_dirname "$0")/.."

IFS=: PYTHON_BUILD_DEFINITIONS=($PYTHON_BUILD_DEFINITIONS ${PYTHON_BUILD_ROOT:-$PYTHON_BUILD_INSTALL_PREFIX}/share/python-build)
IFS="$OLDIFS"

PYTHON_BUILD_ROOTを指定。

pyenv-build
parse_options "$@"

for option in "${OPTIONS[@]}"; do
  case "$option" in
  "h" | "help" )
    version
    echo
    usage 0
    ;;
  "definitions" )
    list_definitions
    exit 0
    ;;
  "k" | "keep" )
    KEEP_BUILD_PATH=true
    ;;
  "v" | "verbose" )
    VERBOSE=true
    ;;
  "p" | "patch" )
    HAS_PATCH=true
    ;;
  "g" | "debug" )
    DEBUG=true
    # Disable optimization (#808)
    PYTHON_CFLAGS="-O0 ${PYTHON_CFLAGS}"
    ;;
  "4" | "ipv4")
    IPV4=true
    ;;
  "6" | "ipv6")
    IPV6=true
    ;;
  "version" )
    version
    exit 0
    ;;
  esac
done

parse_optionsで(python-build $KEEP $VERBOSE $HAS_PATCH $DEBUG "$DEFINITION" "$PREFIX" || STATUS="$?")を処理して、OPTIONSARGUMENTSを作成。DEBUGまでは前者で、DEFINITION以降は後者で処理される。
オプションの確認。

pyenv-build
[ "${#ARGUMENTS[@]}" -eq 2 ] || usage 1 >&2

DEFINITION_PATH="${ARGUMENTS[0]}"
if [ -z "$DEFINITION_PATH" ]; then
  usage 1 >&2
elif [ ! -f "$DEFINITION_PATH" ]; then
  for DEFINITION_DIR in "${PYTHON_BUILD_DEFINITIONS[@]}"; do
    if [ -f "${DEFINITION_DIR}/${DEFINITION_PATH}" ]; then
      DEFINITION_PATH="${DEFINITION_DIR}/${DEFINITION_PATH}"
      break
    fi
  done

  if [ ! -f "$DEFINITION_PATH" ]; then
    echo "python-build: definition not found: ${DEFINITION_PATH}" >&2
    exit 2
  fi
fi

ARGUMENTSの最初は、DEFINITION_PATH
DEFINITION3.7.0であったが、ここで、
DEFINITION_PATHは、DEFINITION_PATH=/usr/local/Cellar/pyenv/2.3.1/plugins/python-build/bin/../share/python-build/3.7.0となる。

pyenv-build
PREFIX_PATH="${ARGUMENTS[1]}"
if [ -z "$PREFIX_PATH" ]; then
  usage 1 >&2
elif [ "${PREFIX_PATH#/}" = "$PREFIX_PATH" ]; then
  PREFIX_PATH="${PWD}/${PREFIX_PATH}"
fi

ARGUMENTSの2番目は、PREFIX_PATH

pyenv-build
if [ -z "$MAKE" ]; then
  if [ "FreeBSD" = "$(uname -s)" ]; then
    if [ "$(echo $1 | sed 's/-.*$//')" = "jruby" ]; then
      export MAKE="gmake"
    else
      if [ "$(uname -r | sed 's/[^[:digit:]].*//')" -lt 10 ]; then
        export MAKE="gmake"
      else
        export MAKE="make"
      fi
    fi
  else
    export MAKE="make"
  fi
fi

makeコマンドの指定。

pyenv-build
if [ -z "$PYTHON_BUILD_MIRROR_URL" ]; then
  PYTHON_BUILD_MIRROR_URL="https://pyenv.github.io/pythons"
  PYTHON_BUILD_DEFAULT_MIRROR=1
else
  PYTHON_BUILD_MIRROR_URL="${PYTHON_BUILD_MIRROR_URL%/}"
  PYTHON_BUILD_DEFAULT_MIRROR=
fi

ミラーサイトの指定。

pyenv-build
ARIA2_OPTS="${PYTHON_BUILD_ARIA2_OPTS} ${IPV4+--disable-ipv6=true} ${IPV6+--disable-ipv6=false}"
CURL_OPTS="${PYTHON_BUILD_CURL_OPTS} ${IPV4+--ipv4} ${IPV6+--ipv6}"
WGET_OPTS="${PYTHON_BUILD_WGET_OPTS} ${IPV4+--inet4-only} ${IPV6+--inet6-only}"

curl,wget等のオプションの指定。

pyenv-build
# Add an option to build a debug version of Python (#11)
if [ -n "$DEBUG" ]; then
  package_option python configure --with-pydebug
fi

# python-build: Specify `--libdir` on configure to fix build on openSUSE (#36)
package_option python configure --libdir="${PREFIX_PATH}/lib"
pyenv-build(package_option)
package_option() {
  local package_name="$1"
  local command_name="$2"
  local variable="$(capitalize "${package_name}_${command_name}")_OPTS_ARRAY"
  local array="$variable[@]"
  shift 2
  local value=( "${!array}" "$@" )
  eval "$variable=( \"\${value[@]}\" )"
}

configureオプションを指定。

pyenv-build
# python-build: Set `RPATH` if `--enable-shared` was given (#65, #66, #82)
if [[ "$CONFIGURE_OPTS $PYTHON_CONFIGURE_OPTS" == *"--enable-shared"* ]]; then
  # The ld on Darwin embeds the full paths to each dylib by default
  if [[ "$LDFLAGS" != *"-rpath="* ]] && ! is_mac; then
    export LDFLAGS="-Wl,-rpath=${PREFIX_PATH}/lib ${LDFLAGS}"
  fi
fi

# python-build: Set `RPATH` if --shared` was given for PyPy (#244)
if [[ "$PYPY_OPTS" == *"--shared"* ]]; then
  export LDFLAGS="-Wl,-rpath=${PREFIX_PATH}/lib ${LDFLAGS}"
fi

LDFLAGSの指定。

pyenv-build
SEED="$(date "+%Y%m%d%H%M%S").$$"
LOG_PATH="${TMP}/python-build.${SEED}.log"
PYTHON_BIN="${PREFIX_PATH}/bin/python$(python_bin_suffix "${DEFINITION_PATH##*/}")"
CWD="$(pwd)"

if [ -z "$PYTHON_BUILD_BUILD_PATH" ]; then
  BUILD_PATH="${TMP}/python-build.${SEED}"
else
  BUILD_PATH="$PYTHON_BUILD_BUILD_PATH"
fi

BUILD_PATHの指定。

pyenv-build
export LDFLAGS="-L${PREFIX_PATH}/lib ${LDFLAGS}"
export CPPFLAGS="-I${PREFIX_PATH}/include ${CPPFLAGS}"

LDFLAGSおよびCPPFLAGSの指定。

trap build_failed ERR
mkdir -p "$BUILD_PATH"
source "$DEFINITION_PATH"
[ -z "${KEEP_BUILD_PATH}" ] && rm -fr "$BUILD_PATH"
trap - ERR

source "$DEFINITION_PATH"ここが本体

/usr/local/Cellar/pyenv/2.3.1/plugins/python-build/bin/../share/python-build/3.7.0

今回は、3.7.0を例に見る。

#require_gcc
prefer_openssl11
export PYTHON_BUILD_CONFIGURE_WITH_OPENSSL=1
install_package "openssl-1.1.0j" "https://www.openssl.org/source/old/1.1.0/openssl-1.1.0j.tar.gz#31bec6c203ce1a8e93d5994f4ed304c63ccf07676118b6634edded12ad1b3246" mac_openssl --if has_broken_mac_openssl
install_package "readline-8.0" "https://ftpmirror.gnu.org/readline/readline-8.0.tar.gz#e339f51971478d369f8a053a330a190781acb9864cf4c541060f12078948e461" mac_readline --if has_broken_mac_readline
if has_tar_xz_support; then
  install_package "Python-3.7.0" "https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tar.xz#0382996d1ee6aafe59763426cf0139ffebe36984474d0ec4126dd1c40a8b3549" standard verify_py37 copy_python_gdb ensurepip
else
  install_package "Python-3.7.0" "https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tgz#85bb9feb6863e04fb1700b018d9d42d1caac178559ffa453d7e6a436e259fd0d" standard verify_py37 copy_python_gdb ensurepip
fi

気になるのはinstall_packageである。

再び/usr/local/opt/pyenv/bin/python-build

install_package() {
  install_package_using "tarball" 1 "$@"
}
...
install_package_using() {
  local package_type="$1"
  local package_type_nargs="$2"
  local package_name="$3"
  shift 3

  local fetch_args=( "$package_name" "${@:1:$package_type_nargs}" )
  local make_args=( "$package_name" )
  local arg last_arg

  for arg in "${@:$(( $package_type_nargs + 1 ))}"; do
    if [ "$last_arg" = "--if" ]; then
      "$arg" || return 0
    elif [ "$arg" != "--if" ]; then
      make_args["${#make_args[@]}"]="$arg"
    fi
    last_arg="$arg"
  done

  pushd "$BUILD_PATH" >&4
  "fetch_${package_type}" "${fetch_args[@]}"
  make_package "${make_args[@]}"
  popd >&4

  { echo "Installed ${package_name} to ${PREFIX_PATH}"
    echo
  } >&2
}
...
make_package() {
  local package_name="$1"
  shift

  pushd "$package_name" >&4
  setup_builtin_patches "$package_name"
  before_install_package "$package_name"
  build_package "$package_name" $*
  after_install_package "$package_name"
  cleanup_builtin_patches "$package_name"
  fix_directory_permissions
  popd >&4
}

build_packageは、build_package_standard Python-3.7.0を結局は呼ぶことになる。

# Backward Compatibility for standard functionbuild_package_standard() {
  build_package_standard_build "$@"
  build_package_standard_install "$@"
}
...
build_package_standard_build() {
  local package_name="$1"

  if [ "${MAKEOPTS+defined}" ]; then
    MAKE_OPTS="$MAKEOPTS"
  elif [ -z "${MAKE_OPTS+defined}" ]; then
    MAKE_OPTS="-j $(num_cpu_cores)"
  fi

  # Support YAML_CONFIGURE_OPTS, PYTHON_CONFIGURE_OPTS, etc.
  local package_var_name="$(capitalize "${package_name%%-*}")"
  local PACKAGE_CONFIGURE="${package_var_name}_CONFIGURE"
  local PACKAGE_PREFIX_PATH="${package_var_name}_PREFIX_PATH"
  local PACKAGE_CONFIGURE_OPTS="${package_var_name}_CONFIGURE_OPTS"
  local PACKAGE_CONFIGURE_OPTS_ARRAY="${package_var_name}_CONFIGURE_OPTS_ARRAY[@]"
  local PACKAGE_MAKE_OPTS="${package_var_name}_MAKE_OPTS"
  local PACKAGE_MAKE_OPTS_ARRAY="${package_var_name}_MAKE_OPTS_ARRAY[@]"
  local PACKAGE_CFLAGS="${package_var_name}_CFLAGS"

  if [ "$package_var_name" = "PYTHON" ]; then
    use_homebrew || true
    use_tcltk || true
    use_homebrew_readline || use_freebsd_pkg || true
    if is_mac -ge 1014; then
      use_xcode_sdk_zlib || use_homebrew_zlib || true
    else
      use_homebrew_zlib || true
    fi
  fi

  ( if [ "${CFLAGS+defined}" ] || [ "${!PACKAGE_CFLAGS+defined}" ]; then
      export CFLAGS="$CFLAGS ${!PACKAGE_CFLAGS}"
    fi
    if [ -z "$CC" ] && is_mac -ge 1010; then
      export CC=clang
    fi
    ${!PACKAGE_CONFIGURE:-./configure} --prefix="${!PACKAGE_PREFIX_PATH:-$PREFIX_PATH}" \
      $CONFIGURE_OPTS ${!PACKAGE_CONFIGURE_OPTS} "${!PACKAGE_CONFIGURE_OPTS_ARRAY}" || return 1
  ) >&4 2>&1

  { "$MAKE" $MAKE_OPTS ${!PACKAGE_MAKE_OPTS} "${!PACKAGE_MAKE_OPTS_ARRAY}"
  } >&4 2>&1
}

build_package_standard_install() {
  local package_name="$1"
  local package_var_name="$(capitalize "${package_name%%-*}")"

  local PACKAGE_MAKE_INSTALL_OPTS="${package_var_name}_MAKE_INSTALL_OPTS"
  local PACKAGE_MAKE_INSTALL_OPTS_ARRAY="${package_var_name}_MAKE_INSTALL_OPTS_ARRAY[@]"
  local PACKAGE_MAKE_INSTALL_TARGET="${package_var_name}_MAKE_INSTALL_TARGET"

  { "$MAKE" "${!PACKAGE_MAKE_INSTALL_TARGET:-install}" $MAKE_INSTALL_OPTS ${!PACKAGE_MAKE_INSTALL_OPTS} "${!PACKAGE_MAKE_INSTALL_OPTS_ARRAY}"
  } >&4 2>&1
}

なお、configureは、上記の

${!PACKAGE_CONFIGURE:-./configure} --prefix="${!PACKAGE_PREFIX_PATH:-$PREFIX_PATH}" \
      $CONFIGURE_OPTS ${!PACKAGE_CONFIGURE_OPTS} "${!PACKAGE_CONFIGURE_OPTS_ARRAY}"

で行われる。(飽きた)
./configure --prefix=/Users/yo314159265/.pyenv/versions/3.7.0 --libdir=/Users/yo314159265/.pyenv/versions/3.7.0/lib --with-openssl=/usr/local/opt/openssl@1.1 '--with-tcltk-libs=-L/usr/local/opt/tcl-tk/lib -ltcl8.6 -ltk8.6' --with-tcltk-includes=-I/usr/local/opt/tcl-tk/include
ちゃんと、目的の場所--prefix=に出力されることがわかる。
また、makeは、上記の

{ "$MAKE" $MAKE_OPTS ${!PACKAGE_MAKE_OPTS} "${!PACKAGE_MAKE_OPTS_ARRAY}"
  }

で行われる。
make -j 8

感想

pyenv --debug install 3.7.0で結果を追う知恵がついたので、僕は進歩できたと思う。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0