Skip to content

Instantly share code, notes, and snippets.

@shoogle
Last active February 2, 2020 01:19
Show Gist options
  • Save shoogle/3c61f7fcd7a086947034ca90167d857c to your computer and use it in GitHub Desktop.
Save shoogle/3c61f7fcd7a086947034ca90167d857c to your computer and use it in GitHub Desktop.
Cross-platform script to build MuseScore on Windows, Mac or Linux
#!/usr/bin/env bash
source "path_functions.sh" # Bash-Essential-Functions: FilePaths module (BEF-FP)
# Cross-platform symlink function.
function symlink() {
if [[ "$1" == "-s" ]]; then
shift
fi
local target="$1" name="$2"
if [[ "${OSTYPE}" == "msys" ]]; then
# Windows needs to be told if it's a directory or not. Infer that.
# Also: note that we convert `/` to `\`. In this case it's necessary.
local args=""
if [[ -d "${target}" ]]; then
args="/J"
fi
cmd >/dev/null <<EOF
mklink ${args} "${name}" "${target//\//\\}"
EOF
else
ln -s "${target}" "${name}"
fi
}
function safe_symlink() {
local -r target="$1" name="$2"
if [[ -e "${name}" ]] && [[ ! -L "${name}" ]]; then
echo >&2 "Error: File '${name}' exists and is not a symlink. Will not overwrite."
exit 1
fi
rm -f "${name}"
symlink -s "${target}" "${name}"
}
# cross-platform copy function.
# create copy-on-write clones if possible, otherwise make an ordinary copy
function cp() {
if [[ "${OSTYPE}" == darwin* ]]; then
command cp -c "$@" || command cp "$@"
else
command cp --reflink=auto "$@"
fi
}
# function make() {
# local target="$1"
# if [[ "${PLATFORM}" == "macOS" ]]; then
# [[ "${target}" ]] || target="ALL_BUILD"
# xcodebuild -jobs ${CPUS} -project mscore.xcodeproj -configuration Debug -target "${target}"
# else
# local -r make="$(which make 2>/dev/null || which mingw32-make 2>/dev/null || echo "make")"
# command "${make}" -j ${CPUS} "$@"
# fi
# }
function cmake_build() {
# CMAKE's build tool mode simply calls the native build tool that the
# project was configured for. It translates command line options for us.
local cmake_options=("$@") native_options=()
if [[ "${PLATFORM}" == "Windows" ]] || [[ "${PLATFORM}" == "macOS" ]]; then
# users on these platforms can easily upgrade CMAKE to the latest version
cmake_options+=(-j ${CPUS}) # requires cmake version >= 3.12
else
# users on these platforms are stuck with their distribution's CMAKE
native_options+=(-j ${CPUS}) # native tool must accept -j option
fi
cmake --build . "${cmake_options[@]}" -- "${native_options[@]}"
}
function set_variables() {
QTDIR="${QTDIR//\\/\/}" # change Windows-style path to Unix-style
CPUS="$( getconf _NPROCESSORS_ONLN 2>/dev/null \
|| getconf NPROCESSORS_ONLN 2>/dev/null \
|| echo ${NUMBER_OF_PROCESSORS} )"
(($CPUS > 0)) || CPUS=1
bef_fp__setvar_basename "${QTDIR}"
COMPILER="${basename}"
bef_fp__setvar_dirname "${QTDIR}" && bef_fp__setvar_basename "${dirname}"
QT_VERSION="${basename}"
GIT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
case "${OSTYPE}" in
msys ) PLATFORM="Windows" ;;
darwin* ) PLATFORM="macOS" ;;
* ) PLATFORM="other" ;;
esac
if [[ "${PLATFORM}" == "macOS" ]]; then
# PATH set in .bash_profile will not be set in Qt Creator. Set it here:
export PATH="/usr/local/bin:${PATH}" # homebrew
fi
case "${COMPILER}" in
msvc* )
GENERATOR_FAMILY="MSVC"
GENERATOR="Visual Studio 16 2019"
BUILD_64="OFF"
if [[ "${COMPILER}" == msvc*_64 ]]; then
# GENERATOR="${GENERATOR} Win64"
BUILD_64="ON"
fi
;;
mingw* )
GENERATOR_FAMILY="MINGW"
GENERATOR="MinGW Makefiles"
BUILD_64="OFF"
if [[ "${COMPILER}" == mingw*_64 ]]; then
# GENERATOR="${GENERATOR} Win64"
BUILD_64="ON"
fi
;;
esac
}
function set_build_dir() {
BUILD_DIR="builds/${GIT_BRANCH}/${OSTYPE}-${HOSTTYPE}/Qt${QT_VERSION}-${COMPILER}"
}
function link_build_dir() {
local invpath
mkdir -p "${BUILD_DIR}/install"
bef_fp__setvar_invpath "${BUILD_DIR}" && SOURCE_DIR="${invpath}"
safe_symlink "${BUILD_DIR}" "build.debug"
case "${PLATFORM}" in
Windows ) INSTALL_LINK="win32install" ;;
macOS ) INSTALL_LINK="applebuild" ;;
* ) INSTALL_LINK="" ;;
esac
if [[ "${INSTALL_LINK}" ]]; then
safe_symlink "build.debug/install" "${INSTALL_LINK}"
fi
echo "BUILD_DIR: ${BUILD_DIR} SOURCE_DIR: ${SOURCE_DIR} INSTALL_LINK: ${INSTALL_LINK}"
}
function configure() {
local cmake_options=(-DCMAKE_INSTALL_PREFIX=install -DBUILD_WEBENGINE=OFF -DBUILD_WEBEBENGINE=OFF -DDOWNLOAD_ASSETS=ON)
if [[ "${PLATFORM}" == "macOS" ]]; then
cmake_options+=(-DCMAKE_BUILD_TYPE=RELEASE) # don't build mtest
else
cmake_options+=(-DCMAKE_BUILD_TYPE=DEBUG)
fi
case "${PLATFORM}" in
Windows )
cmake_options+=(-G "${GENERATOR}" -DBUILD_64="${BUILD_64}")
if [[ "${GENERATOR_FAMILY}" == "MINGW" ]]; then
cmake_options+=(-DCMAKE_MAKE_PROGRAM=mingw32-make.exe)
fi
;;
macOS ) cmake_options+=(-G "Xcode") ;;
esac
cmake "${cmake_options[@]}" "${SOURCE_DIR}"
# make lrelease # do this in configure step since we don't need to do it on every build
cmake_build --target lrelease
}
function build() {
# make
cmake_build
}
function install() {
# make install
cmake_build --target install
}
function clean() {
rm -rf "${BUILD_DIR}"
}
function copy() {
# copy the build directory of an existing branch to speed up a clean build
local dest_dir branch="$1"
dest_dir="${BUILD_DIR}"
GIT_BRANCH="${branch}" set_build_dir
if [[ ! -d "${BUILD_DIR}" ]]; then
echo "$0: error: no suitable build found for branch '${branch}'." >&2
exit 1
fi
if [[ -d "${dest_dir}" ]]; then
echo "$0: error: build dir not clean for branch '${dest_dir}'." >&2
exit 1
fi
echo "$0: copying build directory for branch '${branch}'." >&2
time cp -rp "${BUILD_DIR}" "${dest_dir}"
echo "$0: deleting 'CMakeCache.txt' to avoid CMake warnings." >&2
rm "${dest_dir}/CMakeCache.txt"
BUILD_DIR="${dest_dir}"
}
function refresh() {
# ensure ignored IDE project files match repository versions
local platform
case "${PLATFORM}" in
Windows ) platform="Windows" ;;
macOS ) platform="Mac" ;;
* ) platform="Linux" ;;
esac
local project_files=(
bef-filepaths.sh
musescore_build.sh
musescore_build_windows.bat
MuseScore_${platform}.config
MuseScore_${platform}.creator
MuseScore_${platform}.creator.user
MuseScore_${platform}.files
MuseScore_${platform}.includes
)
for file in "${project_files[@]}"; do
refresh_file "${file}"
done
refresh_file exclude .git/info/exclude
}
function refresh_file() {
local -r here="${HOME}/ssrc/MuseScore/musescore_build"
local -r stored="${here}/$1" ignored="${2:-$1}"
if [[ -e "${stored}" ]]; then
if [[ "${stored}" -nt "${ignored}" ]] ; then
echo "$0: refreshing file: ${ignored}" >&2
cp -p "${stored}" "${ignored}"
elif [[ "${ignored}" -nt "${stored}" ]]; then
echo "$0: refreshing file: ${stored} (remember to commit)" >&2
cp -p "${ignored}" "${stored}"
fi
else
echo "$0: file not found: ${stored}" >&2
fi
}
function parse_args() {
local arg param
# local get_param='param="${arg:2}" ; [[ "${param}" ]] || { param="$1"; shift ;}'
unset arg_clean arg_configure arg_build arg_install # build steps
unset arg_copy # other arguments
# unset arg_generator arg_build_type # cmake options
while (($# > 0)); do
arg="$1"
shift
case "${arg}" in
# BUILD STEPS
clean )
arg_clean=1
if [[ "$1" == "--copy" ]]; then
# copy another branch's build dir to speed up build
arg_copy="$2"
shift 2
fi
;;
configure ) arg_configure=1 ;;
build ) arg_build=1 ;;
install ) arg_install=1 ;;
# CMAKE OPTIONS
# -G* ) eval "${get_param}" ; arg_generator="${param}" ;;
# -D* ) eval "${get_param}" ; arg_generator="${param}"
* ) echo "$0: unrecognised option: ${arg}" >&2 ;;
esac
done
}
function main() {
set -e # exit immediately on error
if ((${BASH_VERSION:0:1} < 4)); then
echo "$0: Error: Bash version 4 or later required. Please upgrade!" >&2
local mac_help="https://coderwall.com/p/dmuxma/upgrade-bash-on-your-mac-os"
echo "macOS users please read <${mac_help}>." >&2
exit 1
fi
parse_args "$@"
set_variables
set_build_dir
link_build_dir
refresh
if [[ "${arg_clean}" ]]; then
clean
if [[ "${arg_copy}" ]]; then copy "${arg_copy}"; fi
fi
if [[ "${arg_configure}${arg_build}${arg_install}" ]]; then
cd "${BUILD_DIR}" || { echo "$0: error: not yet configured!" >&2 ; exit 1 ;}
if [[ "${arg_configure}" ]]; then configure; fi
if [[ "${arg_build}" ]]; then build ; fi
if [[ "${arg_install}" ]]; then install ; fi
fi
}
printf "%q " "$0" "$@" && echo # display script name and arguments passed by user
main "$@" # call main function
:: I recommend installing Git via Scoop (https://scoop.sh/)
set BASH="%USERPROFILE%\scoop\apps\git-with-openssh\current\bin\bash.exe"
:: If you prefer to install Git manually, uncomment next line
:: set BASH="C:\Program Files (x86)\Git\bin\bash.exe"
%BASH% --login -i -c "./musescore_build.sh %*"
#!/usr/bin/env bash
############# BASH ESSENTIAL FUNCTIONS - FILEPATHS MODULE (BEF-FP) #############
# If copying the contents of this module into your own script be sure to comment
# out the next line, otherwise this library's usage message will be displayed.
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then case "$1" in -l*|--l*) message='
################################## LICENSE #####################################
# Bash Essential Functions - FilePaths module (BEF-FP)
#
# The Bash Essential Functions library is licensed under the MIT License (a.k.a.
# the "Expat License"), details of which are below. Code contributions are
# welcome at <https://github.com/shoogle/bash-essential-functions>.
#
# MIT License
#
# Copyright (c) 2017 Peter Jonas
#
# 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.
################################################################################
#' ;; -h*|-h*) message='
#################################### HELP ######################################
# Bash Essential Functions - FilePaths module (BEF-FP)
#
# Source this file to get access to the functions it contains:
#
# $ source bef-filepaths.sh [--no-convenience-functions]
#
# Functions in this module are named `bef_fp__function_name` to avoid polluting
# the global namespace. After souring the file you can list the functions with:
#
# $ declare -F | grep "bef_fp" # `declare -f` includes function definitions!
#
# BEF modules provide convenience functions that accept data piped from STDIN
# and echo the result to STDOUT, which can be saved using command substitution:
#
# $ myvar="$(pipeline | bef_fp__function_name "${args}")" # convenient!
#
# However, this is not recommended because command substitution strips trailing
# newlines, which, although rarely used, are valid characters in Unix filenames.
# Instead, you should consider using the "setvar" functions:
#
# $ declare funcvar && bef_fp__setvar_funcvar "${arg}" # faster & safer!
#
# This sets the variable "funcvar" to the output of the first function, without
# losing any trailing newline characters. Using the setvar function is faster
# than command substitution because it avoids spawning a subshell. Most setvar
# functions do not accept piped data because this could spawn a subshell.
#
# If you want to disable the convenience functions entirely and keep only setvar
# functions then source the file with the `--no-convenience-functions` option.
################################################################################
#' ;; *) message="
#################################### USAGE #####################################
# Bash Essential Functions - FilePaths module (BEF-FP)
#
# This script is a Bash function library and should be sourced rather than run.
#
# Usage: .|source bef-filepaths.sh [--no-convenience-functions]
#
# However, it may be run with the following options to display info messages:
#
# Usage: [bash] ./bef-filepaths.sh [-h|--help] [-l|--license]
#
# Please submit code contributions and bug reports to the upstream repository
# at <https://github.com/shoogle/bash-essential-functions>. Thanks!
################################################################################
#" ;; esac; IFS=$'\n' read -d '' -n ${#message} -a lines <<<"${message}
#" ; for ((i=1;i<${#lines[@]}-2;i++)); do echo "${lines[i]:2}"; done; exit; fi
################################################################################
if [[ "$1" != "--no-convenience-functions" ]]; then
function bef_fp__basename() {
local path basename \
&& bef_fp__setvar_path_from_args_or_stdin "$@" \
&& bef_fp__setvar_basename "${path}" \
&& echo "${basename}"
}
function bef_fp__dirname() {
local path dirname \
&& bef_fp__setvar_path_from_args_or_stdin "$@" \
&& bef_fp__setvar_dirname "${path}" \
&& echo "${dirname}"
}
# Appends $PWD to $path if $path is relative (does not start with '/'). $path
# does not have to exist. Output is not normalized or canonicalized.
function bef_fp__absolute_path() {
local path abspath \
&& bef_fp__setvar_path_from_args_or_stdin "$@" \
&& bef_fp__setvar_abspath "${path}" \
&& echo "${abspath}"
}
function bef_fp__normalized_path() {
local path normpath \
&& bef_fp__setvar_path_from_args_or_stdin "$@" \
&& bef_fp__setvar_normpath "${path}" \
&& echo "${normpath}"
}
function bef_fp__inverse_path() {
local path invpath \
&& bef_fp__setvar_path_from_args_or_stdin "$@" \
&& bef_fp__setvar_invpath "${path}" \
&& echo "${invpath}"
}
function bef_fp__canonical_path() {
local path canonpath \
&& bef_fp__setvar_path_from_args_or_stdin "$@" \
&& bef_fp__setvar_canonpath "${path}" \
&& echo "${canonpath}"
}
fi # [[ "$1" != "--no-convenience-functions" ]]
# set $path variable in caller shell with input from $1 or STDIN
# Usage: local path && bef_fp__setvar_path_from_args_or_stdin "$@"
function bef_fp__setvar_path_from_args_or_stdin() {
if (($# > 0)); then
path="$1"
else
read -d '' -r path # path from STDIN (path may contain newlines)
path="${path%$'\n'}" # strip extra newline added by read -d ''
fi
}
# set $nodes array in caller shell with result of splitting $1 on "/"
# Usage: local nodes=() && bef_fp__setvar_nodes_array "${path}"
function bef_fp__setvar_nodes_array() {
local length=${#1} IFS="/" # split path on "/"
# read $path into array. Only read $lenght chars to avoid extra '\n' chars.
((length == 0)) || read -d '' -n $length -r -a nodes <<<"$1"
}
# set $basename variable in caller shell with basename of $1. Avoids subshell.
# Usage: local basename && bef_fp__setvar_basename "${path}"
function bef_fp__setvar_basename() {
local path="$1"
if [[ "${path}" =~ ^/+$ ]]; then
basename="/" # path consists only of slashes
return # '/////' gives '/'
fi
while [[ "${path}" == *"/" ]]; do
path="${path%/}" # strip trailing slash(es): 'foo/bar//' goes to 'foo/bar'
done
# return everything after final slash, or everything if there are no slashes
basename="${path##*/}" # 'foo/bar' or 'bar' gives 'bar'
}
# set $dirname variable in caller function dirname of $1. Avoids subshell.
# Usage: local dirname && bef_fp__setvar_dirname "${path}"
function bef_fp__setvar_dirname() {
local path="$1"
if [[ "${path}" =~ ^/*[^/]*/*$ ]]; then # path only has one node: //foo///
if [[ "${path:0:1}" == / ]]; then
dirname="/" # '//foo///', '//foo' or '//' gives '/' (shortest abs. path)
else
dirname="." # 'foo///', 'foo' or '' gives '.' (shortest relative path)
fi
return
fi
while [[ "${path}" == */ ]]; do # strip any trailing slash(es)
path="${path%/}" # 'foo///bar///' goes to 'foo///bar'
done
# only keep characters before the final slash, or everything if no slashes
path="${path%/*}" # 'foo///bar' goes to 'foo//', 'foo' goes to 'foo'
while [[ "${path}" == */ ]]; do
path="${path%/}" # strip trailing slash(es): 'foo//' goes to 'foo'
done
dirname="${path}"
}
# Prepends $PWD to $path if $path is relative (does not start with '/'). $path
# does not have to exist. Output is not normalized or canonicalized.
function bef_fp__setvar_abspath() {
local path="$1"
if [[ "${path:0:1}" == "/" ]]; then
abspath="${path}"
else
abspath="${PWD}/${path}"
fi
}
# Removes unnecessary slashes and resolves '.' and '..' to return the shortest
# path equivalent to the one given as input. The path does not have to exist.
function bef_fp__setvar_normpath() {
local path="$1" basename dirname abs skip=0
[[ "${path:0:1}" == "/" ]] && abs="/" || abs="" # relative or absolute path?
while [[ ! "${path}" =~ ^\.?/*$ ]]; do
bef_fp__setvar_basename "${path}"
bef_fp__setvar_dirname "${path}"
if [[ "${basename}" == "." ]]; then
: # ignore node
elif [[ "${basename}" == ".." ]]; then
((skip++)) # skip next node that isn't '.' or '..' or already skipped
elif ((skip > 0)); then
((skip--)) # skipping this node due to prior '..'
else
normpath="${basename}/${normpath}" # add node to normpath
fi
path="${dirname}" # move to next node
done
if [[ ! "${abs}" ]]; then
while ((skip > 0)); do
normpath="../${normpath}"
((skip--))
done
fi
if [[ "${normpath}" ]]; then
normpath="${abs}${normpath%/}" # strip trailing slash
elif [[ "${abs}" ]]; then
normpath="/"
else
normpath="."
fi
}
# Gives location of working directory relative to specified path
# The path does not have to exist.
function bef_fp__setvar_invpath() {
local path="$1" compath="$2" normpath bk=0 fd=0
if [[ ! "${compath}" ]]; then
compath="${PWD}"
fi
bef_fp__setvar_normpath "${path}"
path="${normpath}"
if [[ "${path:0:1}" == "/" ]]; then
invpath="${PWD}"
elif [[ "${path}" == "." ]]; then
invpath="."
else
while [[ "${path}" != "." ]]; do
bef_fp__setvar_dirname "${path}"
bef_fp__setvar_basename "${path}"
if [[ "${basename}" == ".." ]]; then
((bk++))
else
((fd++))
fi
path="${dirname}"
done
while ((bk > 0)); do
bef_fp__setvar_basename "${compath}"
invpath="${basename}/${invpath}"
bef_fp__setvar_dirname "${compath}"
compath="${dirname}"
((bk--))
done
while ((fd > 0)); do
invpath="../${invpath}"
((fd--))
done
invpath="${invpath%/}" # strip trailing slash
fi
}
function bef_fp__setvar_canondirpath() {
local dirpath="$1"
# `cd` to $dirpath and back again. ${OLDPWD} created automatically by shell.
CDPATH="" cd -P "${dirpath}" \
&& canondirpath="${PWD}" \
&& CDPATH="" cd "${OLDPWD}" \
|| echo "${FUNCNAME[0]}: error: unable to access: ${dirpath}" >&2
}
# Prints physical path without symbolic links. ${path} must exist on system.
function bef_fp__setvar_canonpath() {
local path="$1" canonpath canondirpath
if [[ -d "${path}" ]]; then
bef_fp__setvar_canondirpath "${path}"
canonpath="${canondirpath}"
elif [[ ! -e "${path}" ]]; then
if [[ -L "${path}" ]]; then
echo "${FUNCNAME[0]}: error: no target found for symlink: ${path}" >&2
return 2
else
echo "${FUNCNAME[0]}: error: file not found: ${path}" >&2
return 1
fi
else
local basename dirname symlinks=0 max_symlinks=40
while [[ -L "${path}" ]]; do
if ((symlinks++ && symlinks > max_symlinks)); then
echo "${FUNCNAME[0]}: error: too many symlinks: ${path}" >&2
return 3
fi
# Must read the link. Can we avoid using subshell and external binary?
canonpath="$(readlink "${path}")" # XXX: Can we avoid this?
if [[ "${canonpath:0:1}" == "/" ]]; then
path="${canonpath}"
else
bef_fp__setvar_dirname "${path}"
path="${dirname}/${canonpath}"
fi
done
bef_fp__setvar_basename "${path}"
bef_fp__setvar_dirname "${path}"
bef_fp__setvar_canondirpath "${dirname}"
canonpath="${canondirpath}/${basename}"
fi
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment