1# Copyright (C) 2010, 2011, 2012, 2013, 2014, 2018, 2020  Olga Yakovleva <yakovleva.o.v@gmail.com>
2
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License
14# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16import sys
17import os
18import os.path
19import subprocess
20import platform
21import datetime
22import re
23if sys.platform=="win32":
24    if sys.version_info[0]>=3:
25        import winreg
26    else:
27        import _winreg as winreg
28
29def get_version(is_release):
30    next_version="1.2.3"
31    return next_version
32
33def CheckPKGConfig(context):
34    context.Message("Checking for pkg-config... ")
35    result=context.TryAction("pkg-config --version")[0]
36    context.Result(result)
37    return result
38
39def CheckPKG(context,name):
40    context.Message("Checking for {}... ".format(name))
41    result=context.TryAction("pkg-config --exists '{}'".format(name))[0]
42    context.Result(result)
43    return result
44
45def CheckMSVC(context):
46    context.Message("Checking for Visual C++ ... ")
47    result=0
48    version=context.env.get("MSVC_VERSION",None)
49    if version is not None:
50        result=1
51    context.Result(result)
52    return result
53
54def CheckXPCompat(context):
55    context.Message("Checking for Windows XP compatibility ... ")
56    result=0
57    if context.env.get("xp_compat_enabled",False):
58        result=1
59    context.Result(result)
60    return result
61
62def CheckNSIS(context):
63    result=0
64    context.Message("Checking for NSIS")
65    if "NSISDIR" in os.environ:
66        context.env["makensis"]=File(os.path.join(os.environ["NSISDIR"],"makensis.exe"))
67        result=1
68    else:
69        key_name=r"SOFTWARE\NSIS"
70        try:
71            with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,key_name,0,winreg.KEY_READ|winreg.KEY_WOW64_32KEY) as key:
72                context.env["makensis"]=File(os.path.join(winreg.QueryValueEx(key,None)[0],"makensis.exe"))
73                result=1
74        except WindowsError:
75            pass
76    context.Result(result)
77    return result
78
79def CheckWiX(context):
80    result=0
81    context.Message("Checking for WiX toolset")
82    if "WIXTOOLPATH" in os.environ or "WIX" in os.environ:
83        if "WIXTOOLPATH"in os.environ:
84            context.env["WIX"]=os.environ["WIXTOOLPATH"]
85        else:
86            context.env["WIX"]=os.path.join(os.environ["WIX"],"bin")
87        result=1
88    context.Result(result)
89    return result
90
91def validate_spd_version(key,val,env):
92    m=re.match(r"^\d+\.\d+",val)
93    if m is None:
94        raise Exception("Invalid value of spd_version: {}".format(val))
95
96def CheckSpdVersion(ctx):
97    ctx.Message("Checking Speech Dispatcher version ... ")
98    ver=ctx.env.get("spd_version",None)
99    if ver is not None:
100        ctx.Result(ver)
101        return ver
102    res, ver=ctx.TryAction("pkg-config --modversion speech-dispatcher > $TARGET")
103    ver=ver.strip()
104    if not res:
105        src='#include <stdio.h>\n#include <speech-dispatcher/libspeechd_version.h>\nint main() {\nint major=LIBSPEECHD_MAJOR_VERSION;\nint minor=LIBSPEECHD_MINOR_VERSION;\nprintf("%d.%d",major,minor);\nreturn 0;}'
106        res,ver=ctx.TryRun(src,".c")
107    if not res:
108        ctx.Result(res)
109        return res
110    ctx.env["spd_version"]=ver
111    ctx.Result(ver)
112    return ver
113
114def convert_flags(value):
115    return value.split()
116
117def convert_path(value):
118    return value.split(";" if sys.platform=="win32" else ":")
119
120def setup():
121    if sys.platform=="win32":
122        SetOption("warn","no-visual-c-missing")
123    global BUILDDIR,var_cache
124    system=platform.system().lower()
125    BUILDDIR=os.path.join("build",system)
126    var_cache=os.path.join(BUILDDIR,"user.conf")
127    Execute(Mkdir(BUILDDIR))
128    SConsignFile(os.path.join(BUILDDIR,"scons"))
129
130def create_languages_user_var():
131    langs_dir=Dir("#data").Dir("languages")
132    names=[name for name in sorted(os.listdir(langs_dir.path)) if os.path.isdir(langs_dir.Entry(name).path)]
133    langs=[name.lower() for name in names]
134    name_map=dict(zip(names,langs))
135    def_langs=langs
136    if sys.platform!="win32":
137        def_langs=[lang for lang in langs if lang not in["georgian"]]
138        print("Georgian language is skipped because of non-free license")
139    help="Which languages to install"
140    return ListVariable("languages",help,def_langs,langs,name_map)
141
142def create_audio_libs_user_var():
143    libs=["pulse","libao","portaudio"]
144    help="Which audio libraries to use if they are available"
145    return ListVariable("audio_libs",help,libs,libs)
146
147def create_user_vars():
148    args={"DESTDIR":""}
149    args.update(ARGUMENTS)
150    vars=Variables(var_cache,args)
151    vars.Add(BoolVariable("dev","The build will only be used for development: no global installation, run from the source directory, compile helper utilities",False))
152    vars.Add(create_languages_user_var())
153    vars.Add(BoolVariable("enable_mage","Build with MAGE",True))
154    vars.Add(create_audio_libs_user_var())
155    vars.Add("spd_version","Speech dispatcher version",validator=validate_spd_version)
156    vars.Add(BoolVariable("release","Whether we are building a release",True))
157    if sys.platform=="win32":
158        vars.Add(BoolVariable("enable_x64","Additionally build 64-bit versions of all the libraries",True))
159        vars.Add(BoolVariable("enable_xp_compat","Target Windows XP",False))
160        vars.Add(PathVariable("msi_repo","Where the msi packages are kept for reuse",None,PathVariable.PathIsDir))
161    else:
162        vars.Add("prefix","Installation prefix","/usr/local")
163        vars.Add("bindir","Program installation directory","$prefix/bin")
164        vars.Add("libdir","Library installation directory","$prefix/lib")
165        vars.Add("includedir","Header installation directory","$prefix/include")
166        vars.Add("datadir","Data installation directory","$prefix/share")
167        vars.Add("sysconfdir","A directory for configuration files","$prefix/etc")
168        vars.Add("servicedir",".service file installation directory","$datadir/dbus-1/services")
169        vars.Add("DESTDIR","Support for staged installation","")
170        vars.Add(BoolVariable("enable_shared","Build a shared library",True))
171    if sys.platform=="win32":
172        suffixes=["32","64"]
173    else:
174        suffixes=[""]
175    for suffix in suffixes:
176        vars.Add("CPPPATH"+suffix,"List of directories where to search for headers",[],converter=convert_path)
177        vars.Add("LIBPATH"+suffix,"List of directories where to search for libraries",[],converter=convert_path)
178    vars.Add("CPPFLAGS","C/C++ preprocessor flags",[],converter=convert_flags)
179    vars.Add("CCFLAGS","C/C++ compiler flags",["/O2","/GL","/Gw"] if sys.platform=="win32" else ["-O2"],converter=convert_flags)
180    vars.Add("CFLAGS","C compiler flags",[],converter=convert_flags)
181    vars.Add("CXXFLAGS","C++ compiler flags",[],converter=convert_flags)
182    vars.Add("LINKFLAGS","Linker flags",["/LTCG","/OPT:REF","/OPT:ICF"] if sys.platform=="win32" else [],converter=convert_flags)
183    return vars
184
185def create_base_env(user_vars):
186    env_args={"variables":user_vars}
187    if sys.platform=="win32":
188        env_args["tools"]=["newlines"]
189    else:
190        env_args["tools"]=["default","installer"]
191    env_args["tools"].extend(["textfile","library"])
192    env_args["LIBS"]=[]
193    env_args["package_name"]="RHVoice"
194    env_args["CPPDEFINES"]=[("RHVOICE","1")]
195    env=Environment(**env_args)
196    if env["dev"]:
197        env["prefix"]=os.path.abspath("local")
198        env["RPATH"]=env.Dir("$libdir").abspath
199    env["package_version"]=get_version(env["release"])
200    env.Append(CPPDEFINES=("PACKAGE",env.subst(r'\"$package_name\"')))
201    if env["PLATFORM"]=="win32":
202        env.Append(CPPDEFINES=("WIN32",1))
203        env.Append(CPPDEFINES=("UNICODE",1))
204        env.Append(CPPDEFINES=("NOMINMAX",1))
205    env["libcore"]="RHVoice_core"
206    env["libaudio"]="RHVoice_audio"
207    return env
208
209def display_help(env,vars):
210    Help("Type 'scons' to build the package.\n")
211    if sys.platform!="win32":
212        Help("Then type 'scons install' to install it.\n")
213        Help("Type 'scons --clean install' to uninstall the software.\n")
214    Help("You may use the following configuration variables:\n")
215    Help(vars.GenerateHelpText(env))
216
217def clone_base_env(base_env,user_vars,arch=None):
218    args={}
219    if sys.platform=="win32":
220        if arch is not None:
221            args["TARGET_ARCH"]=arch
222        args["tools"]=["msvc","mslink","mslib"]
223    env=base_env.Clone(**args)
224    user_vars.Update(env)
225    if env["PLATFORM"]=="win32":
226        env.AppendUnique(CCFLAGS=["/nologo","/MT"])
227        env.AppendUnique(LINKFLAGS=["/nologo"])
228        env.AppendUnique(CXXFLAGS=["/EHsc"])
229        if env["enable_xp_compat"]:
230            env.Tool("xp_compat")
231    if "gcc" in env["TOOLS"]:
232        env.MergeFlags("-pthread")
233        env.AppendUnique(CXXFLAGS=["-std=c++11"])
234        env.AppendUnique(CFLAGS=["-std=c11"])
235    if sys.platform=="win32":
236        bits="64" if arch.endswith("64") else "32"
237        env["BUILDDIR"]=os.path.join(BUILDDIR,arch)
238        env["CPPPATH"]=env["CPPPATH"+bits]
239        env["LIBPATH"]=env["LIBPATH"+bits]
240    else:
241        env["BUILDDIR"]=BUILDDIR
242    third_party_dir=os.path.join("src","third-party")
243    for path in Glob(os.path.join(third_party_dir,"*"),strings=True):
244        if os.path.isdir(path):
245            env.Prepend(CPPPATH=("#"+path))
246    env.Prepend(CPPPATH=(os.path.join("#"+env["BUILDDIR"],"include"),".",os.path.join("#src","include")))
247    return env
248
249def configure(env):
250    tests={"CheckPKGConfig":CheckPKGConfig,"CheckPKG":CheckPKG,"CheckSpdVersion":CheckSpdVersion}
251    if env["PLATFORM"]=="win32":
252        tests["CheckMSVC"]=CheckMSVC
253        tests["CheckXPCompat"]=CheckXPCompat
254    conf=env.Configure(conf_dir=os.path.join(env["BUILDDIR"],"configure_tests"),
255                       log_file=os.path.join(env["BUILDDIR"],"configure.log"),
256                       config_h=os.path.join(env["BUILDDIR"],"include","configure.h"),
257                       custom_tests=tests)
258    if env["PLATFORM"]=="win32":
259        if not conf.CheckMSVC():
260            print("Error: Visual C++ is not installed")
261            exit(1)
262        print("Visual C++ version is {}".format(env["MSVC_VERSION"]))
263        if env["enable_xp_compat"] and not conf.CheckXPCompat():
264            print("Error: Windows XP compatibility cannot be enabled")
265            exit(1)
266    if not conf.CheckCC():
267        print("The C compiler is not working")
268        exit(1)
269    if not conf.CheckCXX():
270        print("The C++ compiler is not working")
271        exit(1)
272# has_sox=conf.CheckLibWithHeader("sox","sox.h","C",call='sox_init();',autoadd=0)
273# if not has_sox:
274#     print("Error: cannot link with libsox")
275#     exit(1)
276# env.PrependUnique(LIBS="sox")
277    has_giomm=False
278    has_pkg_config=conf.CheckPKGConfig()
279    if has_pkg_config:
280        if "pulse" in env["audio_libs"] and not False:
281            env["audio_libs"].remove("pulse")
282        if "libao" in env["audio_libs"] and not conf.CheckPKG("ao"):
283            env["audio_libs"].remove("libao")
284        if "portaudio" in env["audio_libs"] and not False:
285            env["audio_libs"].remove("portaudio")
286        if env["audio_libs"]:
287            conf.CheckSpdVersion()
288    else:
289        env["audio_libs"]=[]
290#        has_giomm=conf.CheckPKG("giomm-2.4")
291    if env["PLATFORM"]=="win32":
292        env.AppendUnique(LIBS="kernel32")
293    conf.Finish()
294    env.Prepend(LIBPATH=os.path.join("#"+env["BUILDDIR"],"core"))
295    src_subdirs=["third-party","core","lib"]
296    if env["dev"]:
297        src_subdirs.append("utils")
298    src_subdirs.append("audio")
299    src_subdirs.append("test")
300    if env["audio_libs"]:
301        src_subdirs.append("sd_module")
302    env.Prepend(LIBPATH=os.path.join("#"+env["BUILDDIR"],"audio"))
303    if has_giomm:
304        src_subdirs.append("service")
305    if env["PLATFORM"]=="win32":
306        src_subdirs.append("sapi")
307    else:
308        src_subdirs.append("include")
309    return src_subdirs
310
311def build_binaries(base_env,user_vars,arch=None):
312    env=clone_base_env(base_env,user_vars,arch)
313    if env["BUILDDIR"]!=BUILDDIR:
314        Execute(Mkdir(env["BUILDDIR"]))
315    if arch:
316        print("Configuring the build system for {}".format(arch))
317    src_subdirs=configure(env)
318    for subdir in src_subdirs:
319        SConscript(os.path.join("src",subdir,"SConscript"),
320                   variant_dir=os.path.join(env["BUILDDIR"],subdir),
321                   exports={"env":env},
322                   duplicate=0)
323
324def build_for_linux(base_env,user_vars):
325    build_binaries(base_env,user_vars)
326    for subdir in ["data","config"]:
327        SConscript(os.path.join(subdir,"SConscript"),exports={"env":base_env},
328                   variant_dir=os.path.join(BUILDDIR,subdir),
329                   duplicate=0)
330
331def preconfigure_for_windows(env):
332    conf=env.Configure(conf_dir=os.path.join(BUILDDIR,"configure_tests"),
333                       log_file=os.path.join(BUILDDIR,"configure.log"),
334                       custom_tests={"CheckNSIS":CheckNSIS,"CheckWiX":CheckWiX})
335    conf.CheckWiX()
336    conf.CheckNSIS()
337    conf.Finish()
338
339def build_for_windows(base_env,user_vars):
340    preconfigure_for_windows(base_env)
341    build_binaries(base_env,user_vars,"x86")
342    if base_env["enable_x64"]:
343        build_binaries(base_env,user_vars,"x86_64")
344    if "WIX" in base_env:
345        SConscript(os.path.join("src","wininst","SConscript"),
346                   variant_dir=os.path.join(BUILDDIR,"wininst"),
347                   exports={"env":base_env},
348                   duplicate=0)
349    SConscript(os.path.join("data","SConscript"),
350               variant_dir=os.path.join(BUILDDIR,"data"),
351               exports={"env":base_env},
352               duplicate=0)
353    docs=["README.md"]+[os.path.join("licenses",name) for name in os.listdir("licenses") if name!="voices"]
354    for f in docs:
355        base_env.ConvertNewlines(os.path.join(BUILDDIR,f),f)
356    base_env.ConvertNewlinesB(os.path.join(BUILDDIR,"RHVoice.ini"),os.path.join("config","RHVoice.conf"))
357    # env.ConvertNewlinesB(os.path.join(BUILDDIR,"dict.txt"),os.path.join("config","dicts","example.txt"))
358    SConscript(os.path.join("src","nvda-synthDriver","SConscript"),
359               variant_dir=os.path.join(BUILDDIR,"nvda-synthDriver"),
360               exports={"env":base_env},
361               duplicate=0)
362
363setup()
364vars=create_user_vars()
365base_env=create_base_env(vars)
366display_help(base_env,vars)
367vars.Save(var_cache,base_env)
368if sys.platform=="win32":
369    build_for_windows(base_env,vars)
370else:
371    build_for_linux(base_env,vars)
372