osx_utils.sh 16 KB
Newer Older
Matthew Brett's avatar
Matthew Brett committed
1
2
3
4
#!/bin/bash
# Use with ``source osx_utils.sh``
set -e

5
6
7
8
# Get our own location on this filesystem, load common utils
MULTIBUILD_DIR=$(dirname "${BASH_SOURCE[0]}")
source $MULTIBUILD_DIR/common_utils.sh

Matthew Brett's avatar
Matthew Brett committed
9
10
11
12
MACPYTHON_URL=https://www.python.org/ftp/python
MACPYTHON_PY_PREFIX=/Library/Frameworks/Python.framework/Versions
WORKING_SDIR=working

Andrew Murray's avatar
Andrew Murray committed
13
# As of 22 December 2020 - latest Python of each version with binary download
Matthew Brett's avatar
Matthew Brett committed
14
# available.
15
# See: https://www.python.org/downloads/mac-osx/
robbuckley's avatar
robbuckley committed
16
LATEST_2p7=2.7.18
Andrew Murray's avatar
Andrew Murray committed
17
LATEST_3p5=3.5.4
Andrew Murray's avatar
Andrew Murray committed
18
LATEST_3p6=3.6.8
Andrew Murray's avatar
Andrew Murray committed
19
LATEST_3p7=3.7.9
Andrew Murray's avatar
Andrew Murray committed
20
LATEST_3p8=3.8.7
21
LATEST_3p9=3.9.1
Matthew Brett's avatar
Matthew Brett committed
22

Matthew Brett's avatar
Matthew Brett committed
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

function check_python {
    if [ -z "$PYTHON_EXE" ]; then
        echo "PYTHON_EXE variable not defined"
        exit 1
    fi
}

function check_pip {
    if [ -z "$PIP_CMD" ]; then
        echo "PIP_CMD variable not defined"
        exit 1
    fi
}

function check_var {
    if [ -z "$1" ]; then
        echo "required variable not defined"
        exit 1
    fi
}

function get_py_digit {
    check_python
    $PYTHON_EXE -c "import sys; print(sys.version_info[0])"
}

function get_py_mm {
    check_python
    $PYTHON_EXE -c "import sys; print('{0}.{1}'.format(*sys.version_info[0:2]))"
}

function get_py_mm_nodot {
    check_python
    $PYTHON_EXE -c "import sys; print('{0}{1}'.format(*sys.version_info[0:2]))"
}

function get_py_prefix {
    check_python
    $PYTHON_EXE -c "import sys; print(sys.prefix)"
}

function fill_pyver {
    # Convert major or major.minor format to major.minor.micro
    #
    # Hence:
    # 2 -> 2.7.11  (depending on LATEST_2p7 value)
    # 2.7 -> 2.7.11  (depending on LATEST_2p7 value)
    local ver=$1
    check_var $ver
    if [[ $ver =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
        # Major.minor.micro format already
        echo $ver
    elif [ $ver == 2 ] || [ $ver == "2.7" ]; then
        echo $LATEST_2p7
Andrew Murray's avatar
Andrew Murray committed
78
79
80
    elif [ $ver == 3 ] || [ $ver == "3.9" ]; then
        echo $LATEST_3p9
    elif [ $ver == "3.8" ]; then
81
82
        echo $LATEST_3p8
    elif [ $ver == "3.7" ]; then
Andrew Murray's avatar
Andrew Murray committed
83
84
        echo $LATEST_3p7
    elif [ $ver == "3.6" ]; then
Matthew Brett's avatar
Matthew Brett committed
85
86
        echo $LATEST_3p6
    elif [ $ver == "3.5" ]; then
Matthew Brett's avatar
Matthew Brett committed
87
88
        echo $LATEST_3p5
    else
89
        echo "Can't fill version $ver" 1>&2
Matthew Brett's avatar
Matthew Brett committed
90
91
92
93
        exit 1
    fi
}

94
95
96
97
98
99
100
101
102
103
104
function macpython_sdk_list_for_version {
    # return a list of SDK targets supported for a given CPython version
    # Parameters
    #   $py_version (python version in major.minor.extra format)
    # eg
    #  macpython_sdks_for_version 2.7.15
    #  >> 10.6 10.9
    local _ver=$(fill_pyver $1)
    local _major=${_ver%%.*}
    local _return

105
    if [ "${PLAT}" = "arm64" ]; then
106
107
        _return="11.0"
    elif [ "$_major" -eq "2" ]; then
robbuckley's avatar
robbuckley committed
108
        [ $(lex_ver $_ver) -lt $(lex_ver 2.7.18) ] && _return="10.6"
109
110
111
112
113
114
115
116
117
118
119
120
121
        [ $(lex_ver $_ver) -ge $(lex_ver 2.7.15) ] && _return="$_return 10.9"
    elif [ "$_major" -eq "3" ]; then
        [ $(lex_ver $_ver) -lt $(lex_ver 3.8)    ] && _return="10.6"
        [ $(lex_ver $_ver) -ge $(lex_ver 3.6.5)  ] && _return="$_return 10.9"
    else
        echo "Error version=${_ver}, expecting 2.x or 3.x" 1>&2
        exit 1
    fi
    echo $_return
}

function macpython_sdk_for_version {
    # assumes the output of macpython_sdk_list_for_version is a list
Andrew Murray's avatar
Andrew Murray committed
122
    # of SDK versions XX.Y in sorted order, eg "10.6 10.9" or "10.9"
123
124
125
    echo $(macpython_sdk_list_for_version $1) | awk -F' ' '{print $NF}'
}

Matthew Brett's avatar
Matthew Brett committed
126
127
128
129
130
131
132
133
function pyinst_ext_for_version {
    # echo "pkg" or "dmg" depending on the passed Python version
    # Parameters
    #   $py_version (python version in major.minor.extra format)
    #
    # Earlier Python installers are .dmg, later are .pkg.
    local py_version=$1
    check_var $py_version
134
    py_version=$(fill_pyver $py_version)
Matthew Brett's avatar
Matthew Brett committed
135
136
    local py_0=${py_version:0:1}
    if [ $py_0 -eq 2 ]; then
137
        if [ "$(lex_ver $py_version)" -ge "$(lex_ver 2.7.9)" ]; then
Matthew Brett's avatar
Matthew Brett committed
138
139
140
141
142
            echo "pkg"
        else
            echo "dmg"
        fi
    elif [ $py_0 -ge 3 ]; then
Andrew Murray's avatar
Andrew Murray committed
143
		echo "pkg"
Matthew Brett's avatar
Matthew Brett committed
144
145
146
    fi
}

147
function pyinst_fname_for_version {
148
    # echo filename for OSX installer file given Python and minimum
149
    # macOS versions
150
    # Parameters
151
    #   $py_version (Python version in major.minor.extra format)
152
    #   $py_osx_ver: {major.minor | not defined}
153
154
155
    #       if defined, the minimum macOS SDK version that Python is
    #       built for, eg: "10.6" or "10.9", if not defined, infers
    #       this from $py_version using macpython_sdk_for_version
156
    local py_version=$1
157
    local py_osx_ver=${2:-$(macpython_sdk_for_version $py_version)}
158
    local inst_ext=$(pyinst_ext_for_version $py_version)
159
    if [ "${PLAT:-}" == "arm64" ] || [ "${PLAT:-}" == "universal2" ]; then
160
      echo "python-${py_version}-macos11.0.${inst_ext}"
161
162
163
    else
      echo "python-${py_version}-macosx${py_osx_ver}.${inst_ext}"
    fi
164
165
}

166
function get_macpython_arch {
robbuckley's avatar
robbuckley committed
167
168
169
170
171
172
    # echo arch (e.g. intel or x86_64), extracted from the distutils platform tag
    # Parameters
    #   $distutils_plat   PEP425 style platform tag, or if not provided, calls
    #                       the function get_distutils_platform, provided by
    #                       common_utils.sh. Fails if this is not a mac platform
    #
Andrew Murray's avatar
Andrew Murray committed
173
    # Note: MUST only be called after the version of Python used to build the
robbuckley's avatar
robbuckley committed
174
175
    # target wheel has been installed and is on the path
    local distutils_plat=${1:-$(get_distutils_platform)}
Isuru Fernando's avatar
Isuru Fernando committed
176
    if [[ $distutils_plat =~ macosx-(1[0-9]\.[0-9]+)-(.*) ]]; then
177
178
        echo ${BASH_REMATCH[2]}
    else
robbuckley's avatar
robbuckley committed
179
        echo "Error parsing macOS distutils platform '$distutils_plat'"
180
181
182
183
184
        exit 1
    fi
}

function get_macpython_osx_ver {
robbuckley's avatar
robbuckley committed
185
186
187
188
189
190
    # echo minimum macOS version (e.g. 10.9) from the distutils platform tag
    # Parameters
    #   $distutils_plat   PEP425 style platform tag, or if not provided, calls
    #                       the function get_distutils_platform, provided by
    #                       common_utils.sh. Fails if this is not a mac platform
    #
Andrew Murray's avatar
Andrew Murray committed
191
    # Note: MUST only be called after the version of Python used to build the
robbuckley's avatar
robbuckley committed
192
193
    # target wheel has been installed and is on the path
    local distutils_plat=${1:-$(get_distutils_platform)}
Isuru Fernando's avatar
Isuru Fernando committed
194
    if [[ $distutils_plat =~ macosx-(1[0-9]\.[0-9]+)-(.*) ]]; then
195
196
        echo ${BASH_REMATCH[1]}
    else
robbuckley's avatar
robbuckley committed
197
        echo "Error parsing macOS distutils platform '$distutils_plat'"
198
199
200
        exit 1
    fi
}
201

202
function macpython_arch_for_version {
203
204
    # echo arch (intel or x86_64) that a version of Python is expected
    # to be built for
205
    # Parameters
206
207
    #   $py_ver     Python version, in the format (major.minor.patch) for
    #               CPython, or pypy-(major.minor) for PyPy
208
    #   $py_osx_ver minimum macOS version the target Python is built for
209
    #               (major.minor)
210
211
212
213
214
215
216
217
218
    local py_ver=$1
    local py_osx_ver=${2:-$MB_PYTHON_OSX_VER}
    check_var $1
    if [[ $(macpython_impl_for_version $py_ver) == "cp" ]]; then
        if [[ "$py_osx_ver" == "10.6" ]]; then
            echo "intel"
        elif [[ "$py_osx_ver" == "10.9" ]]; then
            echo "x86_64"
        else
219
            echo "Unexpected CPython macOS version: ${py_osx_ver}, supported values: 10.6 and 10.9"
220
221
222
            exit 1
        fi
    else
Rob Buckley's avatar
Rob Buckley committed
223
        echo "x86_64"
224
225
226
227
228
229
    fi
}

function macpython_impl_for_version {
    # echo Python implementation (cp for CPython, pp for PyPy) given a
    # suitably formatted version string
230
231
    # Parameters:
    #     $version : [implementation-]major[.minor[.patch]]
232
233
    #         Python implementation, e.g. "3.6" for CPython or
    #         "pypy-5.4" for PyPy
234
    local version=$1
235
    check_var $1
mattip's avatar
mattip committed
236
    if [[ "$version" =~ ^pypy ]]; then
237
        echo pp
238
    elif [[ "$version" =~ ([0-9\.]+) ]]; then
239
        echo cp
Kyle Stewart's avatar
Kyle Stewart committed
240
    else
Matt Bachmann's avatar
Matt Bachmann committed
241
        echo "config error: Issue parsing this implementation in install_python:"
242
        echo "    version=$version"
Kyle Stewart's avatar
Kyle Stewart committed
243
244
245
246
        exit 1
    fi
}

247
function strip_macpython_ver_prefix {
248
    # strip any implementation prefix from a Python version string
249
250
251
252
253
254
255
256
257
258
259
    # Parameters:
    #     $version : [implementation-]major[.minor[.patch]]
    #         Python implementation, e.g. "3.6" for CPython or
    #         "pypy-5.4" for PyPy
    local version=$1
    check_var $1
    if [[ "$version" =~ (pypy-)?([0-9\.]+) ]]; then
        echo ${BASH_REMATCH[2]}
    fi
}

260
261
262
263
function install_macpython {
    # Install Python and set $PYTHON_EXE to the installed executable
    # Parameters:
    #     $version : [implementation-]major[.minor[.patch]]
mattip's avatar
mattip committed
264
    #         The Python implementation to install, e.g. "3.6", "pypy-5.4" or "pypy3.6-7.2"
265
    #     $py_osx_ver: {major.minor | not defined}
266
267
    #       if defined, the macOS version that CPython is built for, e.g.
    #       "10.6" or "10.9". Ignored for PyPy
268
    local version=$1
269
    local py_osx_ver=$2
270
271
    local impl=$(macpython_impl_for_version $version)
    if [[ "$impl" == "pp" ]]; then
mattip's avatar
mattip committed
272
        install_pypy $version
273
    elif [[ "$impl" == "cp" ]]; then
274
        local stripped_ver=$(strip_macpython_ver_prefix $version)
275
        install_mac_cpython $stripped_ver $py_osx_ver
Kyle Stewart's avatar
Kyle Stewart committed
276
    else
277
        echo "Unexpected Python impl: ${impl}"
Kyle Stewart's avatar
Kyle Stewart committed
278
279
280
281
        exit 1
    fi
}

282
function install_mac_cpython {
Matthew Brett's avatar
Matthew Brett committed
283
    # Installs Python.org Python
284
285
286
    # Parameters
    #   $py_version
    #       Version given in major or major.minor or major.minor.micro e.g
Andrew Murray's avatar
Andrew Murray committed
287
    #       "3" or "3.7" or "3.7.1".
288
    #   $py_osx_ver
289
    #       {major.minor | not defined}
290
    #       if defined, the macOS version that Python is built for, e.g.
291
    #        "10.6" or "10.9"
292
    # sets $PYTHON_EXE variable to Python executable
Matthew Brett's avatar
Matthew Brett committed
293
    local py_version=$(fill_pyver $1)
Rob Buckley's avatar
Rob Buckley committed
294
    local py_osx_ver=$2
295
    local py_stripped=$(strip_ver_suffix $py_version)
296
    local py_inst=$(pyinst_fname_for_version $py_version $py_osx_ver)
Matthew Brett's avatar
Matthew Brett committed
297
298
    local inst_path=$DOWNLOADS_SDIR/$py_inst
    mkdir -p $DOWNLOADS_SDIR
299
    curl $MACPYTHON_URL/$py_stripped/${py_inst} > $inst_path
300
    if [ "${py_inst: -3}" == "dmg" ]; then
Matthew Brett's avatar
Matthew Brett committed
301
302
303
304
305
306
        hdiutil attach $inst_path -mountpoint /Volumes/Python
        inst_path=/Volumes/Python/Python.mpkg
    fi
    sudo installer -pkg $inst_path -target /
    local py_mm=${py_version:0:3}
    PYTHON_EXE=$MACPYTHON_PY_PREFIX/$py_mm/bin/python$py_mm
307
308
309
310
311
    # Install certificates for Python 3.6
    local inst_cmd="/Applications/Python ${py_mm}/Install Certificates.command"
    if [ -e "$inst_cmd" ]; then
        sh "$inst_cmd"
    fi
Matthew Brett's avatar
Matthew Brett committed
312
313
314
315
316
317
318
319
320
321
322
}

function install_virtualenv {
    # Generic install of virtualenv
    # Installs virtualenv into python given by $PYTHON_EXE
    # Assumes virtualenv will be installed into same directory as $PYTHON_EXE
    check_pip
    # Travis VMS install virtualenv for system python by default - force
    # install even if installed already
    $PIP_CMD install virtualenv --ignore-installed
    check_python
323
    VIRTUALENV_CMD="$(dirname $PYTHON_EXE)/virtualenv"
Matthew Brett's avatar
Matthew Brett committed
324
325
326
327
328
329
330
331
332
333
334
335
336
}

function make_workon_venv {
    # Make a virtualenv in given directory ('venv' default)
    # Set $PYTHON_EXE, $PIP_CMD to virtualenv versions
    # Parameter $venv_dir
    #    directory for virtualenv
    local venv_dir=$1
    if [ -z "$venv_dir" ]; then
        venv_dir="venv"
    fi
    venv_dir=`abspath $venv_dir`
    check_python
xoviat's avatar
xoviat committed
337
    $PYTHON_EXE -m virtualenv $venv_dir
Matthew Brett's avatar
Matthew Brett committed
338
339
340
341
342
343
    PYTHON_EXE=$venv_dir/bin/python
    PIP_CMD=$venv_dir/bin/pip
}

function remove_travis_ve_pip {
    # Remove travis installs of virtualenv and pip
xoviat's avatar
xoviat committed
344
345
    # FIXME: What if virtualenv is installed but pip is not?
    if [ "$(sudo which virtualenv)" == /usr/local/bin/virtualenv ] && [ "$(sudo which pip)" == /usr/local/bin/pip ]; then
Matthew Brett's avatar
Matthew Brett committed
346
347
348
349
350
351
352
353
        sudo pip uninstall -y virtualenv;
    fi
    if [ "$(sudo which pip)" == /usr/local/bin/pip ]; then
        sudo pip uninstall -y pip;
    fi
}

function set_py_vars {
354
    # Used by terryfy project; left here for back-compatibility
Matthew Brett's avatar
Matthew Brett committed
355
356
357
358
    export PATH="`dirname $PYTHON_EXE`:$PATH"
    export PYTHON_EXE PIP_CMD
}

359
function get_macpython_environment {
Matthew Brett's avatar
Matthew Brett committed
360
361
    # Set up MacPython environment
    # Parameters:
362
363
    #     $version : [implementation-]major[.minor[.patch]]
    #         The Python implementation to install, e.g. "3.6" or "pypy-5.4"
Matthew Brett's avatar
Matthew Brett committed
364
365
366
    #     $venv_dir : {directory_name|not defined}
    #         If defined - make virtualenv in this directory, set python / pip
    #         commands accordingly
367
    #     $py_osx_ver: {major.minor | not defined}
368
369
    #         if defined, the macOS version that Python is built for, e.g.
    #         "10.6" or "10.9", if not defined, use the version from MB_PYTHON_OSX_VER
Matthew Brett's avatar
Matthew Brett committed
370
371
372
373
374
375
    #
    # Installs Python
    # Sets $PYTHON_EXE to path to Python executable
    # Sets $PIP_CMD to full command for pip (including sudo if necessary)
    # If $venv_dir defined, Sets $VIRTUALENV_CMD to virtualenv executable
    # Puts directory of $PYTHON_EXE on $PATH
376
377
    local version=$1
    local venv_dir=$2
378
    local py_osx_ver=${3:-$MB_PYTHON_OSX_VER}
xoviat's avatar
xoviat committed
379
380
381
382

    if [ "$USE_CCACHE" == "1" ]; then
        activate_ccache
    fi
383

Matthew Brett's avatar
Matthew Brett committed
384
    remove_travis_ve_pip
385
    install_macpython $version $py_osx_ver
Matthew Brett's avatar
Matthew Brett committed
386
387
388
389
    # Assume ensurepip available.
    $PYTHON_EXE -m ensurepip
    PIP_CMD="$PYTHON_EXE -m pip"
    $PIP_CMD install --upgrade pip
xoviat's avatar
xoviat committed
390

Matthew Brett's avatar
Matthew Brett committed
391
392
393
    if [ -n "$venv_dir" ]; then
        install_virtualenv
        make_workon_venv $venv_dir
394
395
396
        source $venv_dir/bin/activate
    else
        export PATH="`dirname $PYTHON_EXE`:$PATH"
Matthew Brett's avatar
Matthew Brett committed
397
    fi
398
    export PYTHON_EXE PIP_CMD
Matthew Brett's avatar
Matthew Brett committed
399
}
400

401
function install_delocate {
402
403
    check_pip
    $PIP_CMD install delocate
404
405
406
407
408
}

function repair_wheelhouse {
    local wheelhouse=$1
    install_delocate
409
410
    delocate-wheel $wheelhouse/*.whl # copies library dependencies into wheel
}
411
412
413
414
415
416
417

function install_pkg_config {
    # Install pkg-config avoiding error from homebrew
    # See :
    # https://github.com/matthew-brett/multibuild/issues/24#issue-221951587
    command -v pkg-config > /dev/null 2>&1 || brew install pkg-config
}
xoviat's avatar
xoviat committed
418
419

function activate_ccache {
xoviat's avatar
xoviat committed
420

xoviat's avatar
xoviat committed
421
422
423
424
425
    brew install ccache
    export PATH=/usr/local/opt/ccache/libexec:$PATH
    export CCACHE_CPP2=1

    # Prove to the developer that ccache is activated
xoviat's avatar
xoviat committed
426
    echo "Using C compiler: $(which clang)"
xoviat's avatar
xoviat committed
427
}
428

429
430
function macos_intel_build_setup {
    # Setup build for single arch x86_64 wheels
431
    export PLAT="x86_64"
432
    export _PYTHON_HOST_PLATFORM="macosx-${MB_PYTHON_OSX_VER}-x86_64"
433
434
435
436
437
438
439
    export CFLAGS+=" -arch x86_64"
    export CXXFLAGS+=" -arch x86_64"
    export ARCHFLAGS+=" -arch x86_64"
    export CPPFLAGS+=" -arch x86_64"
    export LDFLAGS+=" -arch x86_64"
}

440
441
function macos_arm64_build_setup {
    # Setup build for single arch arm_64 wheels
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
    export PLAT="arm64"
    export BUILD_PREFIX=/opt/arm64-builds
    sudo mkdir -p $BUILD_PREFIX
    sudo chown -R $USER $BUILD_PREFIX
    update_env_for_build_prefix
    export _PYTHON_HOST_PLATFORM="macosx-11.0-arm64"
    export CFLAGS+=" -arch arm64"
    export CXXFLAGS+=" -arch arm64"
    export CPPFLAGS+=" -arch arm64"
    export ARCHFLAGS+=" -arch arm64"
    export FCFLAGS+=" -arch arm64"
    export FC=$FC_ARM64
    export LDFLAGS+=" -arch arm64 -L$BUILD_PREFIX/lib -Wl,-rpath,$BUILD_PREFIX/lib ${FC_ARM64_LDFLAGS:-}"
    # This would automatically let autoconf know that we are cross compiling for arm64 darwin
    export host_alias="aarch64-apple-darwin20.0.0"
}

function fuse_macos_intel_arm64 {
    local wheelhouse=$(abspath ${WHEEL_SDIR:-wheelhouse})
461
    local py_osx_ver=$(echo ${MB_PYTHON_OSX_VER} | sed "s/\./_/g")
Isuru Fernando's avatar
Isuru Fernando committed
462
    mkdir -p tmp_fused_wheelhouse
463
    for whl in $wheelhouse/*.whl; do
Isuru Fernando's avatar
Isuru Fernando committed
464
       if [[ "$whl" == *macosx_${py_osx_ver}_x86_64.whl ]]; then
465
466
           whl_base=$(echo $whl | rev | cut -c 23- | rev)
           if [[ -f "${whl_base}macosx_11_0_arm64.whl" ]]; then
Isuru Fernando's avatar
Isuru Fernando committed
467
               delocate-fuse $whl "${whl_base}macosx_11_0_arm64.whl" -w tmp_fused_wheelhouse
Isuru Fernando's avatar
Isuru Fernando committed
468
               mv tmp_fused_wheelhouse/$(basename $whl) $wheelhouse/$(basename ${whl_base})macosx_${py_osx_ver}_universal2.whl
469
               # Since we want one wheel that's installable for testing we are deleting the *_x86_64 wheel.
470
471
472
473
474
475
476
477
478
479
               # We are not deleting arm64 wheel because the size is lower and homebrew/conda-forge python
               # will use them by default
               rm $whl
           fi
       fi
    done
}

function wrap_wheel_builder {
    if [[ "${PLAT:-}" == "universal2" ]]; then
480
        (macos_intel_build_setup && $@)
481
        rm -rf *-stamp
482
        (macos_arm64_build_setup && $@)
Andrew Murray's avatar
Andrew Murray committed
483
        fuse_macos_intel_arm64
484
    elif [[ "${PLAT:-}" == "arm64" ]]; then
485
        (macos_arm64_build_setup && $@)
486
487
488
489
    else
        $@
    fi
}