1# SCons build recipe for the GPSD project
2
3# Important targets:
4#
5# build      - build the software (default)
6# dist       - make distribution tarball (requires GNU tar)
7# install    - install programs, libraries, and manual pages
8# uninstall  - undo an install
9#
10# check      - run regression and unit tests.
11# audit      - run code-auditing tools
12# testbuild  - test-build the code from a tarball
13# website    - refresh the website
14# release    - ship a release
15#
16# --clean    - clean all normal build targets
17#
18# Setting the DESTDIR environment variable will prefix the install destinations
19# without changing the --prefix prefix.
20
21# Unfinished items:
22# * Out-of-directory builds: see http://www.scons.org/wiki/UsingBuildDir
23# * Coveraging mode: gcc "-coverage" flag requires a hack
24#   for building the python bindings
25# * Python 3 compatibility in this recipe
26
27# Since SCons 3.0.0 forces print_function on us, it needs to be unconditional.
28# This is recognized to be a bug in SCons, but we need to live with it for now,
29# and we'll need this for eventual Python 3 compatibility, anyway.
30# Python requires this to precede any non-comment code.
31from __future__ import print_function
32
33import ast
34import functools
35import glob
36import imp         # for imp.find_module('gps'), imp deprecated in 3.4
37import operator
38import os
39import pickle
40import re
41# replacement for functions from the commands module, which is deprecated.
42import subprocess
43import sys
44import time
45from distutils import sysconfig
46import SCons
47
48
49# Release identification begins here.
50#
51# Actual releases follow the normal X.Y or X.Y.Z scheme.  The version
52# number in git between releases has the form X.Y~dev, when it is
53# expected that X.Y will be the next actual release.  As an example,
54# when 3.20 is the last release, and 3.20.1 is the expected next
55# release, the version in git will be 3.20.1~dev.  Note that ~ is used,
56# because there is some precedent, ~ is an allowed version number in
57# the Debian version rules, and it does not cause confusion with
58# whether - separates components of the package name, separates the
59# name from the version, or separates version componnents.
60#
61# Keep in sync with gps/__init__.py
62# There are about 16 files with copies of the version number; make
63# sure to update all of them.
64#
65# package version
66gpsd_version = "3.20"
67# client library version
68libgps_version_current = 25
69libgps_version_revision = 0
70libgps_version_age = 0
71libgps_version = "%d.%d.%d" % (libgps_version_current, libgps_version_age,
72                               libgps_version_revision)
73#
74# Release identification ends here
75
76# Hosting information (mainly used for templating web pages) begins here
77# Each variable foo has a corresponding @FOO@ expanded in .in files.
78# There are no project-dependent URLs or references to the hosting site
79# anywhere else in the distribution; preserve this property!
80annmail = "gpsd-announce@nongnu.org"
81bugtracker = "https://gitlab.com/gpsd/gpsd/issues"
82cgiupload = "root@thyrsus.com:/var/www/cgi-bin/"
83clonerepo = "git@gitlab.com:gpsd/gpsd.git"
84devmail = "gpsd-dev@lists.nongnu.org"
85download = "http://download-mirror.savannah.gnu.org/releases/gpsd/"
86formserver = "www@thyrsus.com"
87gitrepo = "git@gitlab.com:gpsd/gpsd.git"
88ircchan = "irc://chat.freenode.net/#gpsd"
89mailman = "https://lists.nongnu.org/mailman/listinfo/"
90mainpage = "https://gpsd.io"
91projectpage = "https://gitlab.com/gpsd/gpsd"
92scpupload = "garyemiller@dl.sv.nongnu.org:/releases/gpsd/"
93sitename = "GPSD"
94sitesearch = "gpsd.io"
95tiplink = "<a href='https://www.patreon.com/esr'>" \
96          "leave a remittance at Patreon</a>"
97tipwidget = '<p><a href="https://www.patreon.com/esr">' \
98            'Donate here to support continuing development.</a></p>'
99usermail = "gpsd-users@lists.nongnu.org"
100webform = "http://www.thyrsus.com/cgi-bin/gps_report.cgi"
101website = "https://gpsd.io/"
102# Hosting information ends here
103
104# gpsd needs Scons version at least 2.3
105EnsureSConsVersion(2, 3, 0)
106# gpsd needs Python version at least 2.6
107EnsurePythonVersion(2, 6)
108
109
110PYTHON_SYSCONFIG_IMPORT = 'from distutils import sysconfig'
111
112# Utility productions
113
114
115def Utility(target, source, action, **kwargs):
116    target = env.Command(target=target, source=source, action=action, **kwargs)
117    env.AlwaysBuild(target)
118    env.Precious(target)
119    return target
120
121
122def UtilityWithHerald(herald, target, source, action, **kwargs):
123    if not env.GetOption('silent'):
124        action = ['@echo "%s"' % herald] + action
125    return Utility(target=target, source=source, action=action, **kwargs)
126
127
128def _getstatusoutput(cmd, nput=None, shell=True, cwd=None, env=None):
129    pipe = subprocess.Popen(cmd, shell=shell, cwd=cwd, env=env,
130                            stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
131    (output, errout) = pipe.communicate(input=nput)
132    status = pipe.returncode
133    return (status, output)
134
135
136def _getoutput(cmd, nput=None, shell=True, cwd=None, env=None):
137    return _getstatusoutput(cmd, nput, shell, cwd, env)[1]
138
139
140# Spawn replacement that suppresses non-error stderr
141def filtered_spawn(sh, escape, cmd, args, env):
142    proc = subprocess.Popen([sh, '-c', ' '.join(args)],
143                            env=env, close_fds=True, stderr=subprocess.PIPE)
144    _, stderr = proc.communicate()
145    if proc.returncode:
146        sys.stderr.write(stderr)
147    return proc.returncode
148
149#
150# Build-control options
151#
152
153
154# without this, scons will not rebuild an existing target when the
155# source changes.
156Decider('timestamp-match')
157
158# support building with various Python versions.
159sconsign_file = '.sconsign.{}.dblite'.format(pickle.HIGHEST_PROTOCOL)
160SConsignFile(sconsign_file)
161
162# Start by reading configuration variables from the cache
163opts = Variables('.scons-option-cache')
164
165systemd_dir = '/lib/systemd/system'
166systemd = os.path.exists(systemd_dir)
167
168# Set distribution-specific defaults here
169imloads = True
170
171boolopts = (
172    # GPS protocols
173    ("ashtech",       True,  "Ashtech support"),
174    ("earthmate",     True,  "DeLorme EarthMate Zodiac support"),
175    ("evermore",      True,  "EverMore binary support"),
176    ("fury",          True,  "Jackson Labs Fury and Firefly support"),
177    ("fv18",          True,  "San Jose Navigation FV-18 support"),
178    ("garmin",        True,  "Garmin kernel driver support"),
179    ("garmintxt",     True,  "Garmin Simple Text support"),
180    ("geostar",       True,  "Geostar Protocol support"),
181    ("greis",         True,  "Javad GREIS support"),
182    ("itrax",         True,  "iTrax hardware support"),
183    ("mtk3301",       True,  "MTK-3301 support"),
184    ("navcom",        True,  "Navcom NCT support"),
185    ("nmea0183",      True,  "NMEA0183 support"),
186    ("nmea2000",      True,  "NMEA2000/CAN support"),
187    ("oncore",        True,  "Motorola OnCore chipset support"),
188    ("sirf",          True,  "SiRF chipset support"),
189    ("skytraq",       True,  "Skytraq chipset support"),
190    ("superstar2",    True,  "Novatel SuperStarII chipset support"),
191    ("tnt",           True,  "True North Technologies support"),
192    ("tripmate",      True,  "DeLorme TripMate support"),
193    ("tsip",          True,  "Trimble TSIP support"),
194    ("ublox",         True,  "u-blox Protocol support"),
195    # Non-GPS protocols
196    ("aivdm",         True,  "AIVDM support"),
197    ("gpsclock",      True,  "GPSClock support"),
198    ("isync",         True,  "Spectratime iSync LNRClok/GRCLOK support"),
199    ("ntrip",         True,  "NTRIP support"),
200    ("oceanserver",   True,  "OceanServer support"),
201    ("passthrough",   True,  "build support for passing through JSON"),
202    ("rtcm104v2",     True,  "rtcm104v2 support"),
203    ("rtcm104v3",     True,  "rtcm104v3 support"),
204    # Time service
205    ("oscillator",    True,  "Disciplined oscillator support"),
206    # Export methods
207    ("dbus_export",   True,  "enable DBUS export support"),
208    ("shm_export",    True,  "export via shared memory"),
209    ("socket_export", True,  "data export over sockets"),
210    # Communication
211    ("bluez",         True,  "BlueZ support for Bluetooth devices"),
212    ("netfeed",       True,  "build support for handling TCP/IP data sources"),
213    ('usb',           True,  "libusb support for USB devices"),
214    # Other daemon options
215    ("control_socket", True,  "control socket for hotplug notifications"),
216    ("force_global",  False, "force daemon to listen on all addressses"),
217    ("systemd",       systemd, "systemd socket activation"),
218    # Client-side options
219    ("clientdebug",   True,  "client debugging support"),
220    ("libgpsmm",      True,  "build C++ bindings"),
221    ("ncurses",       True,  "build with ncurses"),
222    ("qt",            True,  "build Qt bindings"),
223    # Daemon options
224    ("controlsend",   True,  "allow gpsctl/gpsmon to change device settings"),
225    ("reconfigure",   True,  "allow gpsd to change device settings"),
226    ("squelch",       False, "squelch gpsd_log/gpsd_hexdump to save cpu"),
227    # Build control
228    ("coveraging",    False, "build with code coveraging enabled"),
229    ("debug",         False, "include debug information in build"),
230    ("gpsdclients",   True,  "gspd client programs"),
231    ("gpsd",          True,  "gpsd itself"),
232    ("implicit_link", imloads, "implicit linkage is supported in shared libs"),
233    ("magic_hat", sys.platform.startswith('linux'),
234     "special Linux PPS hack for Raspberry Pi et al"),
235    ("manbuild",      True,  "build help in man and HTML formats"),
236    ("minimal", False, "turn off every option not set on the command line"),
237    ("nostrip",       False, "don't symbol-strip binaries at link time"),
238    ("profiling",     False, "build with profiling enabled"),
239    ("python",        True,  "build Python support and modules."),
240    ("shared",        True,  "build shared libraries, not static"),
241    ("timeservice",   False, "time-service configuration"),
242    ("xgps",          True,  "include xgps and xgpsspeed."),
243    # Test control
244    ("slow",          False, "run tests with realistic (slow) delays"),
245)
246
247# now step on the boolopts just read from '.scons-option-cache'
248for (name, default, helpd) in boolopts:
249    opts.Add(BoolVariable(name, helpd, default))
250
251# Gentoo, Fedora, opensuse systems use uucp for ttyS* and ttyUSB*
252if os.path.exists("/etc/gentoo-release"):
253    def_group = "uucp"
254else:
255    def_group = "dialout"
256
257nonboolopts = (
258    ("gpsd_group",       def_group,     "privilege revocation group"),
259    ("gpsd_user",        "nobody",      "privilege revocation user",),
260    ("max_clients",      '64',          "maximum allowed clients"),
261    ("max_devices",      '4',           "maximum allowed devices"),
262    ("prefix",           "/usr/local",  "installation directory prefix"),
263    ("python_coverage",  "coverage run", "coverage command for Python progs"),
264    ("python_libdir",    "",            "Python module directory prefix"),
265    ("qt_versioned",     "",            "version for versioned Qt"),
266    ("sysroot",          "",            "cross-development system root"),
267    ("target",           "",            "cross-development target"),
268    ("target_python",    "python",      "target Python version as command"),
269)
270
271# now step on the non boolopts just read from '.scons-option-cache'
272for (name, default, helpd) in nonboolopts:
273    opts.Add(name, helpd, default)
274
275pathopts = (
276    ("bindir",              "bin",           "application binaries directory"),
277    ("docdir",              "share/doc",     "documents directory"),
278    ("includedir",          "include",       "header file directory"),
279    ("libdir",              "lib",           "system libraries"),
280    ("mandir",              "share/man",     "manual pages directory"),
281    ("pkgconfig",           "$libdir/pkgconfig", "pkgconfig file directory"),
282    ("sbindir",             "sbin",          "system binaries directory"),
283    ("sysconfdir",          "etc",           "system configuration directory"),
284    ("udevdir",             "/lib/udev",     "udev rules directory"),
285)
286
287# now step on the path options just read from '.scons-option-cache'
288for (name, default, helpd) in pathopts:
289    opts.Add(PathVariable(name, helpd, default, PathVariable.PathAccept))
290
291#
292# Environment creation
293#
294import_env = (
295    # Variables used by programs invoked during the build
296    "DISPLAY",         # Required for dia to run under scons
297    "GROUPS",          # Required by gpg
298    "HOME",            # Required by gpg
299    "LOGNAME",         # LOGNAME is required for the flocktest production.
300    'PATH',            # Required for ccache and Coverity scan-build
301    'CCACHE_DIR',      # Required for ccache
302    'CCACHE_RECACHE',  # Required for ccache (probably there are more)
303    # pkg-config (required for crossbuilds at least, and probably pkgsrc)
304    'PKG_CONFIG_LIBDIR',
305    'PKG_CONFIG_PATH',
306    'PKG_CONFIG_SYSROOT_DIR',
307    # Variables for specific packaging/build systems
308    "MACOSX_DEPLOYMENT_TARGET",  # MacOSX 10.4 (and probably earlier)
309    'STAGING_DIR',               # OpenWRT and CeroWrt
310    'STAGING_PREFIX',            # OpenWRT and CeroWrt
311    'CWRAPPERS_CONFIG_DIR',      # pkgsrc
312    # Variables used in testing
313    'WRITE_PAD',       # So we can test WRITE_PAD values on the fly.
314)
315
316envs = {}
317for var in import_env:
318    if var in os.environ:
319        envs[var] = os.environ[var]
320envs["GPSD_HOME"] = os.getcwd()
321
322env = Environment(tools=["default", "tar", "textfile"], options=opts, ENV=envs)
323
324#  Minimal build turns off every option not set on the command line,
325if ARGUMENTS.get('minimal'):
326    for (name, default, helpd) in boolopts:
327        # Ensure gpsd and gpsdclients are always enabled unless explicitly
328        # turned off.
329        if ((default is True and
330             not ARGUMENTS.get(name) and
331             name not in ("gpsd", "gpsdclients"))):
332            env[name] = False
333
334# Time-service build = stripped-down with some diagnostic tools
335if ARGUMENTS.get('timeservice'):
336    timerelated = ("gpsd",
337                   "ipv6",
338                   "magic_hat",
339                   "mtk3301",    # For the Adafruit HAT
340                   "ncurses",
341                   "nmea0183",   # For generic hats of unknown type.
342                   "oscillator",
343                   "socket_export",
344                   "ublox",      # For the Uputronics board
345                   )
346    for (name, default, helpd) in boolopts:
347        if ((default is True and
348             not ARGUMENTS.get(name) and
349             name not in timerelated)):
350            env[name] = False
351
352# Many drivers require NMEA0183 - in case we select timeserver/minimal
353# followed by one of these.
354for driver in ('ashtech',
355               'earthmate',
356               'fury',
357               'fv18',
358               'gpsclock',
359               'mtk3301',
360               'oceanserver',
361               'skytraq',
362               'tnt',
363               'tripmate', ):
364    if env[driver]:
365        env['nmea0183'] = True
366        break
367
368
369# iSync uses ublox underneath, so we force to enable it
370if env['isync']:
371    env['ublox'] = True
372
373opts.Save('.scons-option-cache', env)
374
375for (name, default, helpd) in pathopts:
376    env[name] = env.subst(env[name])
377
378env['VERSION'] = gpsd_version
379env['SC_PYTHON'] = sys.executable  # Path to SCons Python
380
381# Set defaults from environment.  Note that scons doesn't cope well
382# with multi-word CPPFLAGS/LDFLAGS/SHLINKFLAGS values; you'll have to
383# explicitly quote them or (better yet) use the "=" form of GNU option
384# settings.
385# Scons also uses different internal names than most other build-systems.
386# So we rely on MergeFlags/ParseFlags to do the right thing for us.
387env['STRIP'] = "strip"
388env['PKG_CONFIG'] = "pkg-config"
389for i in ["AR", "CC", "CXX", "LD",
390          "PKG_CONFIG", "STRIP", "TAR"]:
391    if i in os.environ:
392        j = i
393        if i == "LD":
394            i = "SHLINK"
395        env[i] = os.getenv(j)
396for i in ["ARFLAGS", "CFLAGS", "CXXFLAGS", "LDFLAGS", "SHLINKFLAGS",
397          "CPPFLAGS", "CCFLAGS", "LINKFLAGS"]:
398    if i in os.environ:
399        env.MergeFlags(Split(os.getenv(i)))
400
401
402# Keep scan-build options in the environment
403for key, value in os.environ.items():
404    if key.startswith('CCC_'):
405        env.Append(ENV={key: value})
406
407# Placeholder so we can kluge together something like VPATH builds.
408# $SRCDIR replaces occurrences for $(srcdir) in the autotools build.
409# scons can get confused if this is not a full path
410env['SRCDIR'] = os.getcwd()
411
412# We may need to force slow regression tests to get around race
413# conditions in the pty layer, especially on a loaded machine.
414if env["slow"]:
415    env['REGRESSOPTS'] = "-S"
416else:
417    env['REGRESSOPTS'] = ""
418
419if env.GetOption("silent"):
420    env['REGRESSOPTS'] += " -Q"
421
422
423def announce(msg):
424    if not env.GetOption("silent"):
425        print(msg)
426
427
428# DESTDIR environment variable means user prefix the installation root.
429DESTDIR = os.environ.get('DESTDIR', '')
430
431
432def installdir(idir, add_destdir=True):
433    # use os.path.join to handle absolute paths properly.
434    wrapped = os.path.join(env['prefix'], env[idir])
435    if add_destdir:
436        wrapped = os.path.normpath(DESTDIR + os.path.sep + wrapped)
437    wrapped.replace("/usr/etc", "/etc")
438    wrapped.replace("/usr/lib/systemd", "/lib/systemd")
439    return wrapped
440
441
442# Honor the specified installation prefix in link paths.
443if env["sysroot"]:
444    env.Prepend(LIBPATH=[env["sysroot"] + installdir('libdir',
445                add_destdir=False)])
446
447# Give deheader a way to set compiler flags
448if 'MORECFLAGS' in os.environ:
449    env.Append(CFLAGS=Split(os.environ['MORECFLAGS']))
450
451# Don't change CCFLAGS if already set by environment.
452if 'CCFLAGS' in os.environ:
453    announce('Warning: CCFLAGS from environment overriding scons settings')
454else:
455    # Should we build with profiling?
456    if env['profiling']:
457        env.Append(CCFLAGS=['-pg'])
458        env.Append(LDFLAGS=['-pg'])
459    # Should we build with coveraging?
460    if env['coveraging']:
461        env.Append(CFLAGS=['-coverage'])
462        env.Append(LDFLAGS=['-coverage'])
463        env.Append(LINKFLAGS=['-coverage'])
464    # Should we build with debug symbols?
465    if env['debug']:
466        env.Append(CCFLAGS=['-g3'])
467    # Should we build with optimisation?
468    if env['debug'] or env['coveraging']:
469        env.Append(CCFLAGS=['-O0'])
470    else:
471        env.Append(CCFLAGS=['-O2'])
472
473# Cross-development
474
475devenv = (("ADDR2LINE", "addr2line"),
476          ("AR", "ar"),
477          ("AS", "as"),
478          ("CXX", "c++"),
479          ("CXXFILT", "c++filt"),
480          ("CPP", "cpp"),
481          ("GXX", "g++"),
482          ("CC", "gcc"),
483          ("GCCBUG", "gccbug"),
484          ("GCOV", "gcov"),
485          ("GPROF", "gprof"),
486          ("LD", "ld"),
487          ("NM", "nm"),
488          ("OBJCOPY", "objcopy"),
489          ("OBJDUMP", "objdump"),
490          ("RANLIB", "ranlib"),
491          ("READELF", "readelf"),
492          ("SIZE", "size"),
493          ("STRINGS", "strings"),
494          ("STRIP", "strip"))
495
496if env['target']:
497    for (name, toolname) in devenv:
498        env[name] = env['target'] + '-' + toolname
499
500if env['sysroot']:
501    env.MergeFlags({"CFLAGS": ["--sysroot=%s" % env['sysroot']]})
502    env.MergeFlags({"LINKFLAGS": ["--sysroot=%s" % env['sysroot']]})
503
504
505# Build help
506def cmp(a, b):
507    return (a > b) - (a < b)
508
509
510Help("""Arguments may be a mixture of switches and targets in any order.
511Switches apply to the entire build regardless of where they are in the order.
512Important switches include:
513
514    prefix=/usr     probably what packagers want
515
516Options are cached in a file named .scons-option-cache and persist to later
517invocations.  The file is editable.  Delete it to start fresh.  Current option
518values can be listed with 'scons -h'.
519""" + opts.GenerateHelpText(env, sort=cmp))
520
521# Configuration
522
523
524def CheckPKG(context, name):
525    context.Message('Checking pkg-config for %s... ' % name)
526    ret = context.TryAction('%s --exists \'%s\''
527                            % (env['PKG_CONFIG'], name))[0]
528    context.Result(ret)
529    return ret
530
531
532# Stylesheet URLs for making HTML and man pages from DocBook XML.
533docbook_url_stem = 'http://docbook.sourceforge.net/release/xsl/current/'
534docbook_man_uri = docbook_url_stem + 'manpages/docbook.xsl'
535docbook_html_uri = docbook_url_stem + 'html/docbook.xsl'
536
537
538def CheckXsltproc(context):
539    context.Message('Checking that xsltproc can make man pages... ')
540    ofp = open("man/xmltest.xml", "w")
541    ofp.write('''
542       <refentry id="foo.1">
543      <refmeta>
544        <refentrytitle>foo</refentrytitle>
545        <manvolnum>1</manvolnum>
546        <refmiscinfo class='date'>9 Aug 2004</refmiscinfo>
547      </refmeta>
548      <refnamediv id='name'>
549        <refname>foo</refname>
550        <refpurpose>check man page generation from docbook source</refpurpose>
551      </refnamediv>
552    </refentry>
553''')
554    ofp.close()
555    probe = ("xsltproc --encoding UTF-8 --output man/foo.1 --nonet "
556             "--noout '%s' man/xmltest.xml" % (docbook_man_uri,))
557    ret = context.TryAction(probe)[0]
558    os.remove("man/xmltest.xml")
559    if os.path.exists("man/foo.1"):
560        os.remove("man/foo.1")
561    else:
562        # failed to create output
563        ret = False
564    context.Result(ret)
565    return ret
566
567
568def CheckCompilerOption(context, option):
569    context.Message('Checking if compiler accepts %s... ' % (option,))
570    old_CFLAGS = context.env['CFLAGS'][:]  # Get a *copy* of the old list
571    context.env.Append(CFLAGS=option)
572    ret = context.TryLink("""
573        int main(int argc, char **argv) {
574            (void) argc; (void) argv;
575            return 0;
576        }
577    """, '.c')
578    if not ret:
579        context.env.Replace(CFLAGS=old_CFLAGS)
580    context.Result(ret)
581    return ret
582
583
584def CheckHeaderDefines(context, file, define):
585    context.Message('Checking if %s supplies %s... ' % (file, define))
586    ret = context.TryLink("""
587        #include <%s>
588        #ifndef %s
589        #error %s is not defined
590        #endif
591        int main(int argc, char **argv) {
592            (void) argc; (void) argv;
593            return 0;
594        }
595    """ % (file, define, define), '.c')
596    context.Result(ret)
597    return ret
598
599
600def CheckSizeOf(context, type):
601    """Check sizeof 'type'"""
602    context.Message('Checking size of ' + type + '... ')
603
604    program = """
605#include <stdlib.h>
606#include <stdio.h>
607
608/*
609 * The CheckSizeOf function does not have a way for the caller to
610 * specify header files to be included to provide the type being
611 * checked.  As a workaround until that is remedied, include the
612 * header required for time_t, which is the sole current use of this
613 * function.
614 */
615#include <time.h>
616
617int main() {
618    printf("%d", (int)sizeof(""" + type + """));
619    return 0;
620}
621"""
622
623    # compile it
624    ret = context.TryCompile(program, '.c')
625    if 0 == ret:
626        announce('ERROR: TryCompile failed\n')
627        # fall back to sizeof(time_t) is 8
628        return '8'
629
630    # run it
631    ret = context.TryRun(program, '.c')
632    context.Result(ret[0])
633    return ret[1]
634
635
636def CheckCompilerDefines(context, define):
637    context.Message('Checking if compiler supplies %s... ' % (define,))
638    ret = context.TryLink("""
639        #ifndef %s
640        #error %s is not defined
641        #endif
642        int main(int argc, char **argv) {
643            (void) argc; (void) argv;
644            return 0;
645        }
646    """ % (define, define), '.c')
647    context.Result(ret)
648    return ret
649
650# Check if this compiler is C11 or better
651
652
653def CheckC11(context):
654    context.Message('Checking if compiler is C11... ')
655    ret = context.TryLink("""
656        #if (__STDC_VERSION__ < 201112L)
657        #error Not C11
658        #endif
659        int main(int argc, char **argv) {
660            (void) argc; (void) argv;
661            return 0;
662        }
663    """, '.c')
664    context.Result(ret)
665    return ret
666
667
668def GetPythonValue(context, name, imp, expr, brief=False):
669    context.Message('Obtaining Python %s... ' % name)
670    context.sconf.cached = 0  # Avoid bogus "(cached)"
671    if not env['target_python']:
672        status, value = 0, str(eval(expr))
673    else:
674        command = [target_python_path, '-c', '%s; print(%s)' % (imp, expr)]
675        try:
676            status, value = _getstatusoutput(command, shell=False)
677        except OSError:
678            status = -1
679        if status == 0:
680            value = value.strip()
681        else:
682            value = ''
683            announce('Python command "%s" failed - disabling Python.\n'
684                     'Python components will NOT be installed' %
685                     command[2])
686            env['python'] = False
687    context.Result('failed' if status else 'ok' if brief else value)
688    return value
689
690
691def GetLoadPath(context):
692    context.Message("Getting system load path... ")
693
694
695cleaning = env.GetOption('clean')
696helping = env.GetOption('help')
697
698# Always set up LIBPATH so that cleaning works properly.
699env.Prepend(LIBPATH=[os.path.realpath(os.curdir)])
700
701# from scons 3.0.5, any changes to env after this, until after
702# config.Finish(), will be lost.  Use config.env until then.
703
704# CheckXsltproc works, but result is incorrectly saved as "no"
705config = Configure(env, custom_tests={
706    'CheckC11': CheckC11,
707    'CheckCompilerDefines': CheckCompilerDefines,
708    'CheckCompilerOption': CheckCompilerOption,
709    'CheckHeaderDefines': CheckHeaderDefines,
710    'CheckPKG': CheckPKG,
711    'CheckSizeOf': CheckSizeOf,
712    'CheckXsltproc': CheckXsltproc,
713    'GetPythonValue': GetPythonValue,
714    })
715
716# Use print, rather than announce, so we see it in -s mode.
717print("This system is: %s" % sys.platform)
718
719libgps_flags = []
720if cleaning or helping:
721    bluezflags = []
722    confdefs = []
723    dbusflags = []
724    htmlbuilder = False
725    manbuilder = False
726    ncurseslibs = []
727    rtlibs = []
728    mathlibs = []
729    tiocmiwait = True  # For cleaning, which works on any OS
730    usbflags = []
731else:
732
733    # OS X aliases gcc to clang
734    # clang accepts -pthread, then warns it is unused.
735    if not config.CheckCC():
736        announce("ERROR: CC doesn't work")
737
738    if ((config.CheckCompilerOption("-pthread") and
739         not sys.platform.startswith('darwin'))):
740        config.env.MergeFlags("-pthread")
741
742    confdefs = ["/* gpsd_config.h generated by scons, do not hand-hack. */\n"]
743
744    confdefs.append('#ifndef GPSD_CONFIG_H\n')
745
746    confdefs.append('#define VERSION "%s"\n' % gpsd_version)
747
748    confdefs.append('#define GPSD_URL "%s"\n' % website)
749
750    # TODO: Move these into an if block only on systems with glibc.
751    # needed for isfinite(), pselect(), etc.
752    # for strnlen() before glibc 2.10
753    # glibc 2.10+ needs 200908L (or XOPEN 700+) for strnlen()
754    # on newer glibc _DEFAULT_SOURCE resets _POSIX_C_SOURCE
755    # we set it just in case
756    confdefs.append('#if !defined(_POSIX_C_SOURCE)')
757    confdefs.append('#define _POSIX_C_SOURCE 200809L')
758    confdefs.append('#endif\n')
759    # for daemon(), cfmakeraw(), strsep() and setgroups()
760    # on glibc 2.19+
761    # may also be added by pkg_config
762    # on linux this eventually sets _USE_XOPEN
763    confdefs.append('#if !defined(_DEFAULT_SOURCE)')
764    confdefs.append('#define _DEFAULT_SOURCE')
765    confdefs.append('#endif\n')
766
767    # sys/un.h, and more, needs __USE_MISC with glibc and osX
768    # __USE_MISC is set by _DEFAULT_SOURCE or _BSD_SOURCE
769
770    # TODO: Many of these are now specified by POSIX.  Check if
771    # defining _XOPEN_SOURCE is necessary, and limit to systems where
772    # it is.
773    # 500 means X/Open 1995
774    # getsid(), isascii(), nice(), putenv(), strdup(), sys/ipc.h need 500
775    # 600 means X/Open 2004
776    # Ubuntu and OpenBSD isfinite() needs 600
777    # 700 means X/Open 2008
778    # glibc 2.10+ needs 700+ for strnlen()
779    # Python.h wants 600 or 700
780
781    # removed 2 Jul 2019 to see if anything breaks...
782    # confdefs.append('#if !defined(_XOPEN_SOURCE)')
783    # confdefs.append('#define _XOPEN_SOURCE 700')
784    # confdefs.append('#endif\n')
785    # Reinstated for FreeBSD (below) 16-Aug-2019
786
787    if sys.platform.startswith('linux'):
788        # for cfmakeraw(), strsep(), etc. on CentOS 7
789        # glibc 2.19 and before
790        # sets __USE_MISC
791        confdefs.append('#if !defined(_BSD_SOURCE)')
792        confdefs.append('#define _BSD_SOURCE')
793        confdefs.append('#endif\n')
794        # for strnlen() and struct ifreq
795        # glibc before 2.10, deprecated in 2.10+
796        confdefs.append('#if !defined(_GNU_SOURCE)')
797        confdefs.append('#define _GNU_SOURCE 1')
798        confdefs.append('#endif\n')
799    elif sys.platform.startswith('darwin'):
800        # strlcpy() and SIGWINCH need _DARWIN_C_SOURCE
801        confdefs.append('#if !defined(_DARWIN_C_SOURCE)')
802        confdefs.append('#define _DARWIN_C_SOURCE 1\n')
803        confdefs.append('#endif\n')
804        # vsnprintf() needs __DARWIN_C_LEVEL >= 200112L
805        # snprintf() needs __DARWIN_C_LEVEL >= 200112L
806        # _DARWIN_C_SOURCE forces __DARWIN_C_LEVEL to 900000L
807        # see <sys/cdefs.h>
808
809        # set internal lib versions at link time.
810        libgps_flags = ["-Wl,-current_version,%s" % libgps_version,
811                        "-Wl,-compatibility_version,%s" % libgps_version,
812                        "-Wl,-install_name,%s/$TARGET" %
813                        installdir('libdir', add_destdir=False)]
814    elif sys.platform.startswith('freebsd') or sys.platform.startswith('dragonfly'):
815        # for isascii(), putenv(), nice(), strptime()
816        confdefs.append('#if !defined(_XOPEN_SOURCE)')
817        confdefs.append('#define _XOPEN_SOURCE 700')
818        confdefs.append('#endif\n')
819        # required to define u_int in sys/time.h
820        confdefs.append('#if !defined(_BSD_SOURCE)')
821        confdefs.append("#define _BSD_SOURCE 1\n")
822        confdefs.append('#endif\n')
823        # required to get strlcpy(), and more, from string.h
824        confdefs.append('#if !defined(__BSD_VISIBLE)')
825        confdefs.append("#define __BSD_VISIBLE 1\n")
826        confdefs.append('#endif\n')
827    elif sys.platform.startswith('openbsd'):
828        # required to define u_int in sys/time.h
829        confdefs.append('#if !defined(_BSD_SOURCE)')
830        confdefs.append("#define _BSD_SOURCE 1\n")
831        confdefs.append('#endif\n')
832        # required to get strlcpy(), and more, from string.h
833        confdefs.append('#if !defined(__BSD_VISIBLE)')
834        confdefs.append("#define __BSD_VISIBLE 1\n")
835        confdefs.append('#endif\n')
836    elif sys.platform.startswith('netbsd'):
837        # required to get strlcpy(), and more, from string.h
838        confdefs.append('#if !defined(_NETBSD_SOURCE)')
839        confdefs.append("#define _NETBSD_SOURCE 1\n")
840        confdefs.append('#endif\n')
841
842    cxx = config.CheckCXX()
843    if not cxx:
844        announce("C++ doesn't work, suppressing libgpsmm and Qt build.")
845        config.env["libgpsmm"] = False
846        config.env["qt"] = False
847
848    # define a helper function for pkg-config - we need to pass
849    # --static for static linking, too.
850    #
851    # Using "--libs-only-L --libs-only-l" instead of "--libs" avoids
852    # a superfluous "-rpath" option in some FreeBSD cases, and the resulting
853    # scons crash.
854    # However, it produces incorrect results for Qt5Network in OSX, so
855    # it can't be used unconditionally.
856    def pkg_config(pkg, shared=env['shared'], rpath_hack=False):
857        libs = '--libs-only-L --libs-only-l' if rpath_hack else '--libs'
858        if not shared:
859            libs += ' --static'
860        return ['!%s --cflags %s %s' % (env['PKG_CONFIG'], libs, pkg)]
861
862    # The actual distinction here is whether the platform has ncurses in the
863    # base system or not. If it does, pkg-config is not likely to tell us
864    # anything useful. FreeBSD does, Linux doesn't. Most likely other BSDs
865    # are like FreeBSD.
866    ncurseslibs = []
867    if config.env['ncurses']:
868        if sys.platform.startswith('dragonfly'):
869            ncurseslibs= [ '-L/usr/local/lib', '-lncurses' ]
870        elif sys.platform.startswith('freebsd'):
871            ncurseslibs = ['-lncurses']
872        elif sys.platform.startswith('openbsd'):
873            ncurseslibs = ['-lcurses']
874        elif sys.platform.startswith('darwin'):
875            ncurseslibs = ['-lcurses']
876        else:
877            announce('Turning off ncurses support, library not found.')
878            config.env['ncurses'] = False
879
880    if config.env['usb']:
881        # In FreeBSD except version 7, USB libraries are in the base system
882        if config.CheckPKG('libusb-1.0'):
883            confdefs.append("#define HAVE_LIBUSB 1\n")
884            try:
885                usbflags = pkg_config('libusb-1.0')
886            except OSError:
887                announce("pkg_config is confused about the state "
888                         "of libusb-1.0.")
889                usbflags = []
890        elif sys.platform.startswith("dragonfly"):
891            confdefs.append("#define HAVE_LIBUSB 1\n")
892            usbflags = [ "-lusb"]
893        elif sys.platform.startswith("freebsd"):
894            confdefs.append("#define HAVE_LIBUSB 1\n")
895            usbflags = ["-lusb"]
896        else:
897            confdefs.append("/* #undef HAVE_LIBUSB */\n")
898            usbflags = []
899    else:
900        confdefs.append("/* #undef HAVE_LIBUSB */\n")
901        usbflags = []
902        config.env["usb"] = False
903
904    if config.CheckLib('librt'):
905        confdefs.append("#define HAVE_LIBRT 1\n")
906        # System library - no special flags
907        rtlibs = ["-lrt"]
908    else:
909        confdefs.append("/* #undef HAVE_LIBRT */\n")
910        rtlibs = []
911
912    # The main reason we check for libm explicitly is to set up the config
913    # environment for CheckFunc for sincos().  But it doesn't hurt to omit
914    # the '-lm' when it isn't appropriate.
915    if config.CheckLib('libm'):
916        mathlibs = ['-lm']
917    else:
918        mathlibs = []
919
920    # FreeBSD uses -lthr for pthreads
921    if config.CheckLib('libthr'):
922        confdefs.append("#define HAVE_LIBTHR 1\n")
923        # System library - no special flags
924        rtlibs += ["-lthr"]
925    else:
926        confdefs.append("/* #undef HAVE_LIBTHR */\n")
927
928    if config.env['dbus_export'] and config.CheckPKG('dbus-1'):
929        confdefs.append("#define HAVE_DBUS 1\n")
930        dbusflags = pkg_config("dbus-1")
931        config.env.MergeFlags(dbusflags)
932    else:
933        confdefs.append("/* #undef HAVE_DBUS */\n")
934        dbusflags = []
935        if config.env["dbus_export"]:
936            announce("Turning off dbus-export support, library not found.")
937        config.env["dbus_export"] = False
938
939    if config.env['bluez'] and config.CheckPKG('bluez'):
940        confdefs.append("#define ENABLE_BLUEZ 1\n")
941        bluezflags = pkg_config('bluez')
942    else:
943        confdefs.append("/* #undef ENABLE_BLUEZ */\n")
944        bluezflags = []
945        if config.env["bluez"]:
946            announce("Turning off Bluetooth support, library not found.")
947        config.env["bluez"] = False
948
949    # in_port_t is not defined on Android
950    if not config.CheckType("in_port_t", "#include <netinet/in.h>"):
951        announce("Did not find in_port_t typedef, assuming unsigned short int")
952        confdefs.append("typedef unsigned short int in_port_t;\n")
953
954    # SUN_LEN is not defined on Android
955    if ((not config.CheckDeclaration("SUN_LEN", "#include <sys/un.h>") and
956         not config.CheckDeclaration("SUN_LEN", "#include <linux/un.h>"))):
957        announce("SUN_LEN is not system-defined, using local definition")
958        confdefs.append("#ifndef SUN_LEN\n")
959        confdefs.append("#define SUN_LEN(ptr) "
960                        "((size_t) (((struct sockaddr_un *) 0)->sun_path) "
961                        "+ strlen((ptr)->sun_path))\n")
962        confdefs.append("#endif /* SUN_LEN */\n")
963
964    if config.CheckHeader(["linux/can.h"]):
965        confdefs.append("#define HAVE_LINUX_CAN_H 1\n")
966        announce("You have kernel CANbus available.")
967    else:
968        confdefs.append("/* #undef HAVE_LINUX_CAN_H */\n")
969        announce("You do not have kernel CANbus available.")
970        config.env["nmea2000"] = False
971
972    # check for C11 or better, and __STDC__NO_ATOMICS__ is not defined
973    # before looking for stdatomic.h
974    if ((config.CheckC11() and
975         not config.CheckCompilerDefines("__STDC_NO_ATOMICS__") and
976         config.CheckHeader("stdatomic.h"))):
977        confdefs.append("#define HAVE_STDATOMIC_H 1\n")
978    else:
979        confdefs.append("/* #undef HAVE_STDATOMIC_H */\n")
980        if config.CheckHeader("libkern/OSAtomic.h"):
981            confdefs.append("#define HAVE_OSATOMIC_H 1\n")
982        else:
983            confdefs.append("/* #undef HAVE_OSATOMIC_H */\n")
984            announce("No memory barriers - SHM export and time hinting "
985                     "may not be reliable.")
986
987    # endian.h is required for rtcm104v2 unless the compiler defines
988    # __ORDER_BIG_ENDIAN__, __ORDER_LITTLE_ENDIAN__ and __BYTE_ORDER__
989    if config.CheckCompilerDefines("__ORDER_BIG_ENDIAN__") \
990       and config.CheckCompilerDefines("__ORDER_LITTLE_ENDIAN__") \
991       and config.CheckCompilerDefines("__BYTE_ORDER__"):
992        confdefs.append("#define HAVE_BUILTIN_ENDIANNESS 1\n")
993        confdefs.append("/* #undef HAVE_ENDIAN_H */\n")
994        confdefs.append("/* #undef HAVE_SYS_ENDIAN_H */\n")
995        announce("Your compiler has built-in endianness support.")
996    else:
997        confdefs.append("/* #undef HAVE_BUILTIN_ENDIANNESS\n */")
998        if config.CheckHeader("endian.h"):
999            confdefs.append("#define HAVE_ENDIAN_H 1\n")
1000            confdefs.append("/* #undef HAVE_SYS_ENDIAN_H */\n")
1001            confdefs.append("/* #undef HAVE_MACHINE_ENDIAN_H */\n")
1002        elif config.CheckHeader("sys/endian.h"):
1003            confdefs.append("/* #undef HAVE_ENDIAN_H */\n")
1004            confdefs.append("#define HAVE_SYS_ENDIAN_H 1\n")
1005            confdefs.append("/* #undef HAVE_MACHINE_ENDIAN_H */\n")
1006        elif config.CheckHeader("machine/endian.h"):
1007            confdefs.append("/* #undef HAVE_ENDIAN_H */\n")
1008            confdefs.append("/* #undef HAVE_SYS_ENDIAN_H */\n")
1009            confdefs.append("#define HAVE_MACHINE_ENDIAN_H 1\n")
1010        else:
1011            confdefs.append("/* #undef HAVE_ENDIAN_H */\n")
1012            confdefs.append("/* #undef HAVE_SYS_ENDIAN_H */\n")
1013            confdefs.append("/* #undef HAVE_MACHINE_ENDIAN_H */\n")
1014            announce("You do not have the endian.h header file. "
1015                     "RTCM V2 support disabled.")
1016            config.env["rtcm104v2"] = False
1017
1018    for hdr in ("arpa/inet",
1019                "netdb",
1020                "netinet/in",
1021                "netinet/ip",
1022                "sys/sysmacros",   # for major(), on linux
1023                "sys/socket",
1024                "sys/un",
1025                "syslog",
1026                "termios",
1027                "winsock2"
1028                ):
1029        if config.CheckHeader(hdr + ".h"):
1030            confdefs.append("#define HAVE_%s_H 1\n"
1031                            % hdr.replace("/", "_").upper())
1032        elif "termios" == hdr:
1033            announce("ERROR: %s.h not found" % hdr)
1034        else:
1035            confdefs.append("/* #undef HAVE_%s_H */\n"
1036                            % hdr.replace("/", "_").upper())
1037
1038    sizeof_time_t = config.CheckSizeOf("time_t")
1039    confdefs.append("#define SIZEOF_TIME_T %s\n" % sizeof_time_t)
1040    announce("sizeof(time_t) is %s" % sizeof_time_t)
1041    if 4 >= int(sizeof_time_t):
1042        announce("WARNING: time_t is too small.  It will fail in 2038")
1043
1044    # check function after libraries, because some function require libraries
1045    # for example clock_gettime() require librt on Linux glibc < 2.17
1046    for f in ("cfmakeraw", "clock_gettime", "daemon", "fcntl", "fork",
1047              "gmtime_r", "inet_ntop", "strlcat", "strlcpy", "strptime"):
1048        if config.CheckFunc(f):
1049            confdefs.append("#define HAVE_%s 1\n" % f.upper())
1050        else:
1051            confdefs.append("/* #undef HAVE_%s */\n" % f.upper())
1052
1053    # Apple may supply sincos() as __sincos(), or not at all
1054    if config.CheckFunc('sincos'):
1055        confdefs.append('#define HAVE_SINCOS\n')
1056    elif config.CheckFunc('__sincos'):
1057        confdefs.append('#define sincos __sincos\n#define HAVE_SINCOS\n')
1058    else:
1059        confdefs.append('/* #undef HAVE_SINCOS */\n')
1060
1061    if config.CheckHeader(["sys/types.h", "sys/time.h", "sys/timepps.h"]):
1062        confdefs.append("#define HAVE_SYS_TIMEPPS_H 1\n")
1063        kpps = True
1064    else:
1065        kpps = False
1066        if config.env["magic_hat"]:
1067            announce("Forcing magic_hat=no since RFC2783 API is unavailable")
1068            config.env["magic_hat"] = False
1069    tiocmiwait = config.CheckHeaderDefines("sys/ioctl.h", "TIOCMIWAIT")
1070    if not tiocmiwait and not kpps:
1071        announce("Neither TIOCMIWAIT nor RFC2783 API is available)")
1072        if config.env["timeservice"]:
1073            announce("ERROR: timeservice specified, but no PPS available")
1074            Exit(1)
1075
1076    # Map options to libraries required to support them that might be absent.
1077    optionrequires = {
1078        "bluez": ["libbluetooth"],
1079        "dbus_export": ["libdbus-1"],
1080    }
1081
1082    keys = list(map(lambda x: (x[0], x[2]), boolopts))  \
1083        + list(map(lambda x: (x[0], x[2]), nonboolopts)) \
1084        + list(map(lambda x: (x[0], x[2]), pathopts))
1085    keys.sort()
1086    for (key, helpd) in keys:
1087        value = config.env[key]
1088        if value and key in optionrequires:
1089            for required in optionrequires[key]:
1090                if not config.CheckLib(required):
1091                    announce("%s not found, %s cannot be enabled."
1092                             % (required, key))
1093                    value = False
1094                    break
1095
1096        confdefs.append("/* %s */" % helpd)
1097        if isinstance(value, bool):
1098            if value:
1099                confdefs.append("#define %s_ENABLE 1\n" % key.upper())
1100            else:
1101                confdefs.append("/* #undef %s_ENABLE */\n" % key.upper())
1102        elif value in (0, "", "(undefined)"):
1103            confdefs.append("/* #undef %s */\n" % key.upper())
1104        else:
1105            if value.isdigit():
1106                confdefs.append("#define %s %s\n" % (key.upper(), value))
1107            else:
1108                confdefs.append("#define %s \"%s\"\n" % (key.upper(), value))
1109
1110    # Simplifies life on hackerboards like the Raspberry Pi
1111    if config.env['magic_hat']:
1112        confdefs.append('''\
1113/* Magic device which, if present, means to grab a static /dev/pps0 for KPPS */
1114#define MAGIC_HAT_GPS   "/dev/ttyAMA0"
1115/* Generic device which, if present, means: */
1116/* to grab a static /dev/pps0 for KPPS */
1117#define MAGIC_LINK_GPS  "/dev/gpsd0"
1118''')
1119
1120    confdefs.append('''\
1121
1122#define GPSD_CONFIG_H
1123#endif /* GPSD_CONFIG_H */
1124''')
1125
1126    manbuilder = htmlbuilder = None
1127    if config.env['manbuild']:
1128        if config.CheckXsltproc():
1129            build = ("xsltproc --encoding UTF-8 --output $TARGET"
1130                     " --nonet %s $SOURCE")
1131            htmlbuilder = build % docbook_html_uri
1132            manbuilder = build % docbook_man_uri
1133        elif WhereIs("xmlto"):
1134            xmlto = "xmlto -o `dirname $TARGET` %s $SOURCE"
1135            htmlbuilder = xmlto % "html-nochunks"
1136            manbuilder = xmlto % "man"
1137        else:
1138            announce("Neither xsltproc nor xmlto found, documentation "
1139                     "cannot be built.")
1140    else:
1141        announce("Build of man and HTML documentation is disabled.")
1142    if manbuilder:
1143        # 18.2. Attaching a Builder to a Construction Environment
1144        config.env.Append(BUILDERS={"Man": Builder(action=manbuilder,
1145                                                   src_suffix=".xml")})
1146        config.env.Append(BUILDERS={"HTML": Builder(action=htmlbuilder,
1147                                                    src_suffix=".xml",
1148                                                    suffix=".html")})
1149
1150    # Determine if Qt network libraries are present, and
1151    # if not, force qt to off
1152    if config.env["qt"]:
1153        qt_net_name = 'Qt%sNetwork' % config.env["qt_versioned"]
1154        qt_network = config.CheckPKG(qt_net_name)
1155        if not qt_network:
1156            config.env["qt"] = False
1157            announce('Turning off Qt support, library not found.')
1158
1159    # If supported by the compiler, enable all warnings except uninitialized
1160    # and missing-field-initializers, which we can't help triggering because
1161    # of the way some of the JSON-parsing code is generated.
1162    # Also not including -Wcast-qual and -Wimplicit-function-declaration,
1163    # because we can't seem to keep scons from passing these to g++.
1164    #
1165    # Do this after the other config checks, to keep warnings out of them.
1166    for option in ('-Wall',
1167                   '-Wcast-align',
1168                   '-Wextra',
1169                   # -Wimplicit-fallthrough same as
1170                   # -Wimplicit-fallthrough=3, except osX hates the
1171                   # second flavor
1172                   '-Wimplicit-fallthrough',
1173                   '-Wmissing-declarations',
1174                   '-Wmissing-prototypes',
1175                   '-Wno-missing-field-initializers',
1176                   '-Wno-uninitialized',
1177                   '-Wpointer-arith',
1178                   '-Wreturn-type',
1179                   '-Wstrict-prototypes',
1180                   '-Wvla',
1181                   ):
1182        if option not in config.env['CFLAGS']:
1183            config.CheckCompilerOption(option)
1184
1185# OSX needs to set the ID for installed shared libraries.  See if this is OSX
1186# and whether we have the tool.
1187
1188# Set up configuration for target Python
1189
1190PYTHON_LIBDIR_CALL = 'sysconfig.get_python_lib()'
1191
1192PYTHON_CONFIG_NAMES = ['CC', 'CXX', 'OPT', 'BASECFLAGS',
1193                       'CCSHARED', 'LDSHARED', 'SO', 'INCLUDEPY', 'LDFLAGS']
1194PYTHON_CONFIG_QUOTED = ["'%s'" % s for s in PYTHON_CONFIG_NAMES]
1195PYTHON_CONFIG_CALL = ('sysconfig.get_config_vars(%s)'
1196                      % ', '.join(PYTHON_CONFIG_QUOTED))
1197
1198
1199# ugly hack from http://www.catb.org/esr/faqs/practical-python-porting/
1200# handle python2/3 strings
1201def polystr(o):
1202    if isinstance(o, str):
1203        return o
1204    if isinstance(o, bytes):
1205        return str(o, encoding='latin-1')
1206    raise ValueError
1207
1208
1209if helping:
1210
1211    # If helping just get usable config info from the local Python
1212    target_python_path = ''
1213    py_config_text = str(eval(PYTHON_CONFIG_CALL))
1214    python_libdir = str(eval(PYTHON_LIBDIR_CALL))
1215
1216else:
1217
1218    if config.env['python'] and config.env['target_python']:
1219        try:
1220            config.CheckProg
1221        except AttributeError:  # Older scons versions don't have CheckProg
1222            target_python_path = config.env['target_python']
1223        else:
1224            target_python_path = config.CheckProg(config.env['target_python'])
1225        if not target_python_path:
1226            announce("Target Python doesn't exist - disabling Python.")
1227            config.env['python'] = False
1228    if config.env['python']:
1229        # Maximize consistency by using the reported sys.executable
1230        target_python_path = config.GetPythonValue('exe path',
1231                                                   'import sys',
1232                                                   'sys.executable',
1233                                                   brief=cleaning)
1234        if config.env['python_libdir']:
1235            python_libdir = config.env['python_libdir']
1236        else:
1237            python_libdir = config.GetPythonValue('lib dir',
1238                                                  PYTHON_SYSCONFIG_IMPORT,
1239                                                  PYTHON_LIBDIR_CALL,
1240                                                  brief=cleaning)
1241            # follow FHS, put in /usr/local/libXX, not /usr/libXX
1242            # may be lib, lib32 or lib64
1243            python_libdir = polystr(python_libdir)
1244            python_libdir = python_libdir.replace("/usr/lib",
1245                                                  "/usr/local/lib")
1246
1247        py_config_text = config.GetPythonValue('config vars',
1248                                               PYTHON_SYSCONFIG_IMPORT,
1249                                               PYTHON_CONFIG_CALL,
1250                                               brief=True)
1251
1252        # aiogps is only available on Python >= 3.6
1253        # FIXME check target_python, not current python
1254        if sys.version_info < (3, 6):
1255            config.env['aiogps'] = False
1256            announce("WARNING: Python too old: "
1257                     "gps/aiogps.py will not be installed\n")
1258        else:
1259            config.env['aiogps'] = True
1260
1261        # check for pyserial
1262        #try:
1263        #    imp.find_module('serial')
1264        #    announce("Python module serial (pyserial) found.")
1265        #except ImportError:
1266        #    # no pycairo, don't build xgps, xgpsspeed
1267        #    announce("WARNING: Python module serial (pyserial) not found.")
1268        #    config.env['xgps'] = False
1269
1270        if config.env['xgps']:
1271            # check for pycairo
1272            #try:
1273            #    imp.find_module('cairo')
1274            #    announce("Python module cairo (pycairo) found.")
1275            #except ImportError:
1276            #    # no pycairo, don't build xgps, xgpsspeed
1277            #    announce("WARNING: Python module cairo (pycairo) not found.")
1278            #    config.env['xgps'] = False
1279
1280            # check for pygobject
1281            #try:
1282            #    imp.find_module('gi')
1283            #    announce("Python module gi (pygobject) found.")
1284            #except ImportError:
1285            #    # no pygobject, don't build xgps, xgpsspeed
1286            #    announce("WARNING: Python module gi (pygobject) not found.")
1287            #    config.env['xgps'] = False
1288
1289            if not config.CheckPKG('gtk+-3.0'):
1290                config.env['xgps'] = False
1291
1292
1293if config.env['python']:  # May have been turned off by error
1294    config.env['PYTHON'] = polystr(target_python_path)
1295    # For regress-driver
1296    config.env['ENV']['PYTHON'] = polystr(target_python_path)
1297    py_config_vars = ast.literal_eval(py_config_text.decode())
1298    py_config_vars = [[] if x is None else x for x in py_config_vars]
1299    python_config = dict(zip(PYTHON_CONFIG_NAMES, py_config_vars))
1300    announce(python_config)
1301
1302
1303env = config.Finish()
1304# All configuration should be finished.  env can now be modified.
1305# NO CONFIG TESTS AFTER THIS POINT!
1306
1307if not (cleaning or helping):
1308
1309    # Be explicit about what we're doing.
1310    changelatch = False
1311    for (name, default, helpd) in boolopts + nonboolopts + pathopts:
1312        if env[name] != env.subst(default):
1313            if not changelatch:
1314                announce("Altered configuration variables:")
1315                changelatch = True
1316            announce("%s = %s (default %s): %s"
1317                     % (name, env[name], env.subst(default), helpd))
1318    if not changelatch:
1319        announce("All configuration flags are defaulted.")
1320
1321    # Gentoo systems can have a problem with the Python path
1322    if os.path.exists("/etc/gentoo-release"):
1323        announce("This is a Gentoo system.")
1324        announce("Adjust your PYTHONPATH to see library directories "
1325                 "under /usr/local/lib")
1326
1327# Should we build the Qt binding?
1328if env["qt"] and env["shared"]:
1329    qt_env = env.Clone()
1330    qt_env.MergeFlags('-DUSE_QT')
1331    qt_env.Append(OBJPREFIX='qt-')
1332    if not (cleaning or helping):
1333        try:
1334            qt_env.MergeFlags(pkg_config(qt_net_name))
1335        except OSError:
1336            announce("pkg_config is confused about the state of %s."
1337                     % qt_net_name)
1338            qt_env = None
1339else:
1340    qt_env = None
1341
1342# Set up for Python coveraging if needed
1343if env['coveraging'] and env['python_coverage'] and not (cleaning or helping):
1344    pycov_default = opts.options[opts.keys().index('python_coverage')].default
1345    pycov_current = env['python_coverage']
1346    pycov_list = pycov_current.split()
1347    if env.GetOption('num_jobs') > 1 and pycov_current == pycov_default:
1348        pycov_list.append('--parallel-mode')
1349    # May need absolute path to coveraging tool if 'PythonXX' is prefixed
1350    pycov_path = env.WhereIs(pycov_list[0])
1351    if pycov_path:
1352        pycov_list[0] = pycov_path
1353        env['PYTHON_COVERAGE'] = ' '.join(pycov_list)
1354        env['ENV']['PYTHON_COVERAGE'] = ' '.join(pycov_list)
1355    else:
1356        announce('Python coverage tool not found - disabling Python coverage.')
1357        env['python_coverage'] = ''  # So we see it in the options
1358
1359# Two shared libraries provide most of the code for the C programs
1360
1361# gpsd client library
1362libgps_sources = [
1363    "ais_json.c",
1364    "bits.c",
1365    "gpsdclient.c",
1366    "gps_maskdump.c",
1367    "gpsutils.c",
1368    "hex.c",
1369    "json.c",
1370    "libgps_core.c",
1371    "libgps_dbus.c",
1372    "libgps_json.c",
1373    "libgps_shm.c",
1374    "libgps_sock.c",
1375    "netlib.c",
1376    "os_compat.c",
1377    "rtcm2_json.c",
1378    "rtcm3_json.c",
1379    "shared_json.c",
1380    "timespec_str.c",
1381]
1382
1383if env['libgpsmm']:
1384    libgps_sources.append("libgpsmm.cpp")
1385
1386# gpsd server library
1387libgpsd_sources = [
1388    "bsd_base64.c",
1389    "crc24q.c",
1390    "driver_ais.c",
1391    "driver_evermore.c",
1392    "driver_garmin.c",
1393    "driver_garmin_txt.c",
1394    "driver_geostar.c",
1395    "driver_greis.c",
1396    "driver_greis_checksum.c",
1397    "driver_italk.c",
1398    "driver_navcom.c",
1399    "driver_nmea0183.c",
1400    "driver_nmea2000.c",
1401    "driver_oncore.c",
1402    "driver_rtcm2.c",
1403    "driver_rtcm3.c",
1404    "drivers.c",
1405    "driver_sirf.c",
1406    "driver_skytraq.c",
1407    "driver_superstar2.c",
1408    "driver_tsip.c",
1409    "driver_ubx.c",
1410    "driver_zodiac.c",
1411    "geoid.c",
1412    "gpsd_json.c",
1413    "isgps.c",
1414    "libgpsd_core.c",
1415    "matrix.c",
1416    "net_dgpsip.c",
1417    "net_gnss_dispatch.c",
1418    "net_ntrip.c",
1419    "ntpshmread.c",
1420    "ntpshmwrite.c",
1421    "packet.c",
1422    "ppsthread.c",
1423    "pseudoais.c",
1424    "pseudonmea.c",
1425    "serial.c",
1426    "subframe.c",
1427    "timebase.c",
1428]
1429
1430if not env["shared"]:
1431    def Library(env, target, sources, version, parse_flags=None):
1432        return env.StaticLibrary(target,
1433                                 [env.StaticObject(s) for s in sources],
1434                                 parse_flags=parse_flags)
1435
1436    def LibraryInstall(env, libdir, sources, version):
1437        return env.Install(libdir, sources)
1438else:
1439    def Library(env, target, sources, version, parse_flags=None):
1440        # Note: We have a possibility of getting either Object or file
1441        # list for sources, so we run through the sources and try to make
1442        # them into SharedObject instances.
1443        obj_list = []
1444        for s in Flatten(sources):
1445            if isinstance(s, str):
1446                obj_list.append(env.SharedObject(s))
1447            else:
1448                obj_list.append(s)
1449        return env.SharedLibrary(target=target,
1450                                 source=obj_list,
1451                                 parse_flags=parse_flags,
1452                                 SHLIBVERSION=version)
1453
1454    def LibraryInstall(env, libdir, sources, version):
1455        # note: osX lib name s/b libgps.VV.dylib
1456        # where VV is libgps_version_current
1457        inst = env.InstallVersionedLib(libdir, sources, SHLIBVERSION=version)
1458        return inst
1459
1460libgps_shared = Library(env=env,
1461                        target="gps",
1462                        sources=libgps_sources,
1463                        version=libgps_version,
1464                        parse_flags=rtlibs + libgps_flags)
1465env.Clean(libgps_shared, "gps_maskdump.c")
1466
1467libgps_static = env.StaticLibrary("gps_static",
1468                                  [env.StaticObject(s)
1469                                   for s in libgps_sources], rtlibs)
1470
1471static_gpsdlib = env.StaticLibrary(
1472    target="gpsd",
1473    source=[env.StaticObject(s, parse_flags=usbflags + bluezflags)
1474            for s in libgpsd_sources],
1475    parse_flags=usbflags + bluezflags)
1476
1477libraries = [libgps_shared]
1478
1479# Only attempt to create the qt library if we have shared turned on
1480# otherwise we have a mismash of objects in library
1481if qt_env:
1482    qtobjects = []
1483    qt_flags = qt_env['CFLAGS']
1484    for c_only in ('-Wmissing-prototypes', '-Wstrict-prototypes',
1485                   '-Wmissing-declarations'):
1486        if c_only in qt_flags:
1487            qt_flags.remove(c_only)
1488    # Qt binding object files have to be renamed as they're built to avoid
1489    # name clashes with the plain non-Qt object files. This prevents the
1490    # infamous "Two environments with different actions were specified
1491    # for the same target" error.
1492    for src in libgps_sources:
1493        if src not in ('ais_json.c', 'json.c', 'libgps_json.c',
1494                       'rtcm2_json.c', 'rtcm3_json.c', 'shared_json.c',
1495                       'timespec_str.c'):
1496            compile_with = qt_env['CXX']
1497            compile_flags = qt_flags
1498        else:
1499            compile_with = qt_env['CC']
1500            compile_flags = qt_env['CFLAGS']
1501        qtobjects.append(qt_env.SharedObject(src,
1502                                             CC=compile_with,
1503                                             CFLAGS=compile_flags))
1504    compiled_qgpsmmlib = Library(qt_env, "Qgpsmm", qtobjects, libgps_version)
1505    libraries.append(compiled_qgpsmmlib)
1506
1507# The libraries have dependencies on system libraries
1508# libdbus appears multiple times because the linker only does one pass.
1509
1510gpsflags = mathlibs + rtlibs + dbusflags
1511gpsdflags = usbflags + bluezflags + gpsflags
1512
1513# Source groups
1514
1515gpsd_sources = [
1516    'dbusexport.c',
1517    'gpsd.c',
1518    'shmexport.c',
1519    'timehint.c'
1520]
1521
1522if env['systemd']:
1523    gpsd_sources.append("sd_socket.c")
1524
1525gpsmon_sources = [
1526    'gpsmon.c',
1527    'monitor_garmin.c',
1528    'monitor_italk.c',
1529    'monitor_nmea0183.c',
1530    'monitor_oncore.c',
1531    'monitor_sirf.c',
1532    'monitor_superstar2.c',
1533    'monitor_tnt.c',
1534    'monitor_ubx.c',
1535]
1536
1537# Production programs
1538
1539gpsd = env.Program('gpsd', gpsd_sources,
1540                   LIBS=['gpsd', 'gps_static'],
1541                   parse_flags=gpsdflags + gpsflags)
1542gpsdecode = env.Program('gpsdecode', ['gpsdecode.c'],
1543                        LIBS=['gpsd', 'gps_static'],
1544                        parse_flags=gpsdflags + gpsflags)
1545gpsctl = env.Program('gpsctl', ['gpsctl.c'],
1546                     LIBS=['gpsd', 'gps_static'],
1547                     parse_flags=gpsdflags + gpsflags)
1548gpsmon = env.Program('gpsmon', gpsmon_sources,
1549                     LIBS=['gpsd', 'gps_static'],
1550                     parse_flags=gpsdflags + gpsflags + ncurseslibs)
1551gpsdctl = env.Program('gpsdctl', ['gpsdctl.c'],
1552                      LIBS=['gps_static'],
1553                      parse_flags=gpsflags)
1554gpspipe = env.Program('gpspipe', ['gpspipe.c'],
1555                      LIBS=['gps_static'],
1556                      parse_flags=gpsflags)
1557gpsrinex = env.Program('gpsrinex', ['gpsrinex.c'],
1558                       LIBS=['gps_static'],
1559                       parse_flags=gpsflags)
1560gps2udp = env.Program('gps2udp', ['gps2udp.c'],
1561                      LIBS=['gps_static'],
1562                      parse_flags=gpsflags)
1563gpxlogger = env.Program('gpxlogger', ['gpxlogger.c'],
1564                        LIBS=['gps_static'],
1565                        parse_flags=gpsflags)
1566lcdgps = env.Program('lcdgps', ['lcdgps.c'],
1567                     LIBS=['gps_static'],
1568                     parse_flags=gpsflags)
1569cgps = env.Program('cgps', ['cgps.c'],
1570                   LIBS=['gps_static'],
1571                   parse_flags=gpsflags + ncurseslibs)
1572ntpshmmon = env.Program('ntpshmmon', ['ntpshmmon.c'],
1573                        LIBS=['gpsd', 'gps_static'],
1574                        parse_flags=gpsflags)
1575ppscheck = env.Program('ppscheck', ['ppscheck.c'],
1576                       LIBS=['gps_static'],
1577                       parse_flags=gpsflags)
1578
1579bin_binaries = []
1580sbin_binaries = []
1581if env["gpsd"]:
1582    sbin_binaries += [gpsd]
1583
1584if env["gpsdclients"]:
1585    sbin_binaries += [gpsdctl]
1586    bin_binaries += [
1587        gps2udp,
1588        gpsctl,
1589        gpsdecode,
1590        gpspipe,
1591        gpsrinex,
1592        gpxlogger,
1593        lcdgps
1594    ]
1595
1596if env["timeservice"] or env["gpsdclients"]:
1597    bin_binaries += [ntpshmmon]
1598    if tiocmiwait:
1599        bin_binaries += [ppscheck]
1600
1601if env["ncurses"] and (env["timeservice"] or env["gpsdclients"]):
1602    bin_binaries += [cgps, gpsmon]
1603else:
1604    announce("WARNING: not building cgps or gpsmon")
1605
1606# Test programs - always link locally and statically
1607test_bits = env.Program('tests/test_bits', ['tests/test_bits.c'],
1608                        LIBS=['gps_static'])
1609test_float = env.Program('tests/test_float', ['tests/test_float.c'])
1610test_geoid = env.Program('tests/test_geoid', ['tests/test_geoid.c'],
1611                         LIBS=['gpsd', 'gps_static'],
1612                         parse_flags=gpsdflags)
1613test_gpsdclient = env.Program('tests/test_gpsdclient',
1614                              ['tests/test_gpsdclient.c'],
1615                              LIBS=['gps_static', 'm'])
1616test_matrix = env.Program('tests/test_matrix', ['tests/test_matrix.c'],
1617                          LIBS=['gpsd', 'gps_static'],
1618                          parse_flags=gpsdflags)
1619test_mktime = env.Program('tests/test_mktime', ['tests/test_mktime.c'],
1620                          LIBS=['gps_static'], parse_flags=mathlibs + rtlibs)
1621test_packet = env.Program('tests/test_packet', ['tests/test_packet.c'],
1622                          LIBS=['gpsd', 'gps_static'],
1623                          parse_flags=gpsdflags)
1624test_timespec = env.Program('tests/test_timespec', ['tests/test_timespec.c'],
1625                            LIBS=['gpsd', 'gps_static'],
1626                            parse_flags=gpsdflags)
1627test_trig = env.Program('tests/test_trig', ['tests/test_trig.c'],
1628                        parse_flags=mathlibs)
1629# test_libgps for glibc older than 2.17
1630test_libgps = env.Program('tests/test_libgps', ['tests/test_libgps.c'],
1631                          LIBS=['gps_static'],
1632                          parse_flags=mathlibs + rtlibs + dbusflags)
1633
1634if not env['socket_export']:
1635    announce("test_json not building because socket_export is disabled")
1636    test_json = None
1637else:
1638    test_json = env.Program(
1639        'tests/test_json', ['tests/test_json.c'],
1640        LIBS=['gps_static'],
1641        parse_flags=mathlibs + rtlibs + usbflags + dbusflags)
1642
1643# duplicate below?
1644test_gpsmm = env.Program('tests/test_gpsmm', ['tests/test_gpsmm.cpp'],
1645                         LIBS=['gps_static'],
1646                         parse_flags=mathlibs + rtlibs + dbusflags)
1647testprogs = [test_bits,
1648             test_float,
1649             test_geoid,
1650             test_gpsdclient,
1651             test_libgps,
1652             test_matrix,
1653             test_mktime,
1654             test_packet,
1655             test_timespec,
1656             test_trig]
1657if env['socket_export']:
1658    testprogs.append(test_json)
1659if env["libgpsmm"]:
1660    testprogs.append(test_gpsmm)
1661
1662# Python programs
1663if not env['python']:
1664    python_built_extensions = []
1665    python_manpages = []
1666    python_misc = []
1667    python_progs = []
1668    python_targets = []
1669else:
1670    # installed python programs
1671    python_progs = ["gegps", "gpscat", "gpsfake", "gpsprof", "ubxtool", "zerk"]
1672    python_deps = {'gpscat': 'packet'}
1673
1674    # python misc helpers and stuff
1675    python_misc = [
1676        "gpscap.py",
1677        "gpssim.py",
1678        "jsongen.py",
1679        "maskaudit.py",
1680        "test_clienthelpers.py",
1681        "test_misc.py",
1682        "test_xgps_deps.py",
1683        "valgrind-audit.py"
1684    ]
1685
1686    if not helping and env['aiogps']:
1687        python_misc.extend(["example_aiogps.py", "example_aiogps_run"])
1688
1689    python_manpages = {
1690        "man/gegps.1": "man/gps.xml",
1691        "man/gpscat.1": "man/gpscat.xml",
1692        "man/gpsfake.1": "man/gpsfake.xml",
1693        "man/gpsprof.1": "man/gpsprof.xml",
1694        "man/ubxtool.1": "man/ubxtool.xml",
1695        "man/zerk.1": "man/zerk.xml",
1696    }
1697
1698    if env['xgps']:
1699        python_progs.extend(["xgps", "xgpsspeed"])
1700        python_manpages.update({
1701            "man/xgps.1": "man/gps.xml",
1702            "man/xgpsspeed.1": "man/gps.xml",
1703        })
1704    else:
1705        announce("WARNING: xgps and xgpsspeed will not be installed")
1706
1707    # Glob() has to be run after all buildable objects defined
1708    python_modules = Glob('gps/*.py', strings=True)
1709
1710    # Remove the aiogps module if not configured
1711    # Don't use Glob's exclude option, since it may not be available
1712    if helping or not env['aiogps']:
1713        try:
1714            python_modules.remove('gps/aiogps.py')
1715        except ValueError:
1716            pass
1717
1718    # Build Python binding
1719    #
1720    python_extensions = {
1721        "gps" + os.sep + "packet": ["crc24q.c",
1722                                    "driver_greis_checksum.c",
1723                                    "driver_rtcm2.c",
1724                                    "gpspacket.c",
1725                                    "hex.c",
1726                                    "isgps.c",
1727                                    "os_compat.c",
1728                                    "packet.c",
1729                                    ]
1730    }
1731
1732    python_env = env.Clone()
1733    # FIXME: build of python wrappers doesn't pickup flags set for coveraging,
1734    # manually add them here
1735    if env['coveraging']:
1736        python_config['BASECFLAGS'] += ' -coverage'
1737        python_config['LDFLAGS'] += ' -coverage'
1738        python_config['LDSHARED'] += ' -coverage'
1739    # in case CC/CXX was set to the scan-build wrapper,
1740    # ensure that we build the python modules with scan-build, too
1741    if env['CC'] is None or env['CC'].find('scan-build') < 0:
1742        python_env['CC'] = python_config['CC']
1743        # As we seem to be changing compilers we must assume that the
1744        # CCFLAGS are incompatible with the new compiler. If we should
1745        # use other flags, the variable or the variable for this
1746        # should be predefined.
1747        if python_config['CC'].split()[0] != env['CC']:
1748            python_env['CCFLAGS'] = ''
1749    else:
1750        python_env['CC'] = (' '.join([env['CC']] +
1751                            python_config['CC'].split()[1:]))
1752    if env['CXX'] is None or env['CXX'].find('scan-build') < 0:
1753        python_env['CXX'] = python_config['CXX']
1754        # As we seem to be changing compilers we must assume that the
1755        # CCFLAGS or CXXFLAGS are incompatible with the new
1756        # compiler. If we should use other flags, the variable or the
1757        # variable for this should be predefined.
1758        if python_config['CXX'].split()[0] != env['CXX']:
1759            python_env['CCFLAGS'] = ''
1760            python_env['CXXFLAGS'] = ''
1761    else:
1762        python_env['CXX'] = (' '.join([env['CXX']] +
1763                             python_config['CXX'].split()[1:]))
1764
1765    ldshared = python_config['LDSHARED']
1766    ldshared = ldshared.replace('-fPIE', '')
1767    ldshared = ldshared.replace('-pie', '')
1768    python_env.Replace(SHLINKFLAGS=[],
1769                       LDFLAGS=python_config['LDFLAGS'],
1770                       LINK=ldshared,
1771                       SHLIBPREFIX="",
1772                       SHLIBSUFFIX=python_config['SO'],
1773                       CPPPATH=[python_config['INCLUDEPY']],
1774                       CPPFLAGS=python_config['OPT'],
1775                       CFLAGS=python_config['BASECFLAGS'],
1776                       CXXFLAGS=python_config['BASECFLAGS'])
1777
1778    python_objects = {}
1779    python_compiled_libs = {}
1780    for ext, sources in python_extensions.items():
1781        python_objects[ext] = []
1782        for src in sources:
1783            python_objects[ext].append(
1784                python_env.NoCache(
1785                    python_env.SharedObject(
1786                        src.split(".")[0] + '-py_' +
1787                        '_'.join(['%s' % (x) for x in sys.version_info]) +
1788                        python_config['SO'], src
1789                    )
1790                )
1791            )
1792        python_compiled_libs[ext] = python_env.SharedLibrary(
1793            ext, python_objects[ext])
1794
1795    # Make sure we know about compiled dependencies
1796    for prog, dep in python_deps.items():
1797        env.Depends(prog, python_compiled_libs['gps' + os.sep + dep])
1798
1799    # Make PEP 241 Metadata 1.0.
1800    # Why not PEP 314 (V1.1) or PEP 345 (V1.2)?
1801    # V1.2 and V1.2 require a Download-URL to an installable binary
1802    python_egg_info_source = """Metadata-Version: 1.0
1803Name: gps
1804Version: %s
1805Summary: Python libraries for the gpsd service daemon
1806Home-page: %s
1807Author: the GPSD project
1808Author-email: %s
1809License: BSD
1810Keywords: GPS
1811Description: The gpsd service daemon can monitor one or more GPS devices \
1812connected to a host computer, making all data on the location and movements \
1813of the sensors available to be queried on TCP port 2947.
1814Platform: UNKNOWN
1815""" % (gpsd_version, website, devmail)
1816    python_egg_info = python_env.Textfile(target="gps-%s.egg-info"
1817                                          % (gpsd_version, ),
1818                                          source=python_egg_info_source)
1819    python_built_extensions = list(python_compiled_libs.values())
1820    python_targets = python_built_extensions + [python_egg_info]
1821
1822
1823env.Command(target="packet_names.h", source="packet_states.h",
1824            action="""
1825    rm -f $TARGET &&\
1826    sed -e '/^ *\\([A-Z][A-Z0-9_]*\\),/s//   \"\\1\",/' <$SOURCE >$TARGET &&\
1827    chmod a-w $TARGET""")
1828
1829env.Textfile(target="gpsd_config.h", source=confdefs)
1830
1831env.Command(target="gps_maskdump.c",
1832            source=["maskaudit.py", "gps.h", "gpsd.h"],
1833            action='''
1834    rm -f $TARGET &&\
1835        $SC_PYTHON $SOURCE -c $SRCDIR >$TARGET &&\
1836        chmod a-w $TARGET''')
1837
1838env.Command(target="ais_json.i", source="jsongen.py", action='''\
1839    rm -f $TARGET &&\
1840    $SC_PYTHON $SOURCE --ais --target=parser >$TARGET &&\
1841    chmod a-w $TARGET''')
1842
1843generated_sources = ['packet_names.h', "ais_json.i",
1844                     'gps_maskdump.c', 'revision.h', 'gpsd.php',
1845                     'gpsd_config.h']
1846
1847# Helper functions for revision hackery
1848
1849
1850def GetMtime(file):
1851    """Get mtime of given file, or 0."""
1852    try:
1853        return os.stat(file).st_mtime
1854    except OSError:
1855        return 0
1856
1857
1858def FileList(patterns, exclusions=None):
1859    """Get list of files based on patterns, minus excluded files."""
1860    files = functools.reduce(operator.add, map(glob.glob, patterns), [])
1861    for file in exclusions:
1862        try:
1863            files.remove(file)
1864        except ValueError:
1865            pass
1866    return files
1867
1868
1869# generate revision.h
1870if 'dev' in gpsd_version:
1871    (st, rev) = _getstatusoutput('git describe --tags')
1872    if st != 0:
1873        # Use timestamp from latest relevant file
1874        files = FileList(['*.c', '*.cpp', '*.h', '*.in', 'SConstruct'],
1875                         generated_sources)
1876        timestamps = map(GetMtime, files)
1877        if timestamps:
1878            from datetime import datetime
1879            latest = datetime.fromtimestamp(sorted(timestamps)[-1])
1880            rev = '%s-%s' % (gpsd_version, latest.isoformat())
1881        else:
1882            rev = gpsd_version  # Paranoia
1883else:
1884    rev = gpsd_version
1885revision = '''/* Automatically generated file, do not edit */
1886#define REVISION "%s"
1887''' % (polystr(rev.strip()),)
1888env.Textfile(target="revision.h", source=[revision])
1889
1890if env['systemd']:
1891    udevcommand = 'TAG+="systemd", ENV{SYSTEMD_WANTS}="gpsdctl@%k.service"'
1892else:
1893    udevcommand = 'RUN+="%s/gpsd.hotplug"' % (env['udevdir'], )
1894
1895
1896# Instantiate some file templates.  We'd like to use the Substfile builtin
1897# but it doesn't seem to work in scons 1.20
1898def substituter(target, source, env):
1899    substmap = (
1900        ('@ANNOUNCE@',   annmail),
1901        ('@BUGTRACKER@', bugtracker),
1902        ('@CGIUPLOAD@',  cgiupload),
1903        ('@CLONEREPO@',  clonerepo),
1904        ('@DATE@',       time.asctime()),
1905        ('@DEVMAIL@',    devmail),
1906        ('@DOWNLOAD@',   download),
1907        ('@FORMSERVER@', formserver),
1908        ('@GITREPO@',    gitrepo),
1909        ('@includedir@',     installdir('includedir', add_destdir=False)),
1910        ('@IRCCHAN@',    ircchan),
1911        ('@libdir@',     installdir('libdir', add_destdir=False)),
1912        ('@LIBGPSVERSION@', libgps_version),
1913        ('@MAILMAN@',    mailman),
1914        ('@MAINPAGE@',   mainpage),
1915        ('@MASTER@',     'DO NOT HAND_HACK! THIS FILE IS GENERATED'),
1916        ('@prefix@',     env['prefix']),
1917        ('@PROJECTPAGE@', projectpage),
1918        ('@QTVERSIONED@', env['qt_versioned']),
1919        ('@SCPUPLOAD@',  scpupload),
1920        ('@SITENAME@',   sitename),
1921        ('@SITESEARCH@', sitesearch),
1922        ('@TIPLINK@',    tiplink),
1923        ('@TIPWIDGET@',  tipwidget),
1924        ('@udevcommand@',    udevcommand),
1925        ('@USERMAIL@',   usermail),
1926        ('@VERSION@',    gpsd_version),
1927        ('@WEBFORM@',    webform),
1928        ('@WEBSITE@',    website),
1929    )
1930
1931    sfp = open(str(source[0]))
1932    content = sfp.read()
1933    sfp.close()
1934    for (s, t) in substmap:
1935        content = content.replace(s, t)
1936    m = re.search("@[A-Z]+@", content)
1937    if m and m.group(0) not in map(lambda x: x[0], substmap):
1938        print("Unknown subst token %s in %s." % (m.group(0), sfp.name),
1939              file=sys.stderr)
1940    tfp = open(str(target[0]), "w")
1941    tfp.write(content)
1942    tfp.close()
1943
1944
1945templated = glob.glob("*.in") + glob.glob("*/*.in") + glob.glob("*/*/*.in")
1946
1947# ignore files in subfolder called 'debian' - the Debian packaging
1948# tools will handle them.
1949templated = [x for x in templated if not x.startswith('debian/')]
1950
1951
1952for fn in templated:
1953    builder = env.Command(source=fn, target=fn[:-3], action=substituter)
1954    env.AddPostAction(builder, 'chmod -w $TARGET')
1955    if fn.endswith(".py.in"):
1956        env.AddPostAction(builder, 'chmod +x $TARGET')
1957
1958# Documentation
1959
1960base_manpages = {
1961    "man/cgps.1": "man/gps.xml",
1962    "man/gps.1": "man/gps.xml",
1963    "man/gps2udp.1": "man/gps2udp.xml",
1964    "man/gpsctl.1": "man/gpsctl.xml",
1965    "man/gpsd.8": "man/gpsd.xml",
1966    "man/gpsdctl.8": "man/gpsdctl.xml",
1967    "man/gpsdecode.1": "man/gpsdecode.xml",
1968    "man/gpsd_json.5": "man/gpsd_json.xml",
1969    "man/gpsinit.8": "man/gpsinit.xml",
1970    "man/gpsmon.1": "man/gpsmon.xml",
1971    "man/gpspipe.1": "man/gpspipe.xml",
1972    "man/gpsrinex.1": "man/gpsrinex.xml",
1973    "man/gpxlogger.1": "man/gpxlogger.xml",
1974    "man/lcdgps.1": "man/gps.xml",
1975    "man/libgps.3": "man/libgps.xml",
1976    "man/libgpsmm.3": "man/libgpsmm.xml",
1977    "man/libQgpsmm.3": "man/libgpsmm.xml",
1978    "man/srec.5": "man/srec.xml",
1979}
1980
1981if env["timeservice"] or env["gpsdclients"]:
1982    base_manpages.update({
1983        "man/ntpshmmon.1": "man/ntpshmmon.xml",
1984    })
1985
1986if tiocmiwait:
1987    base_manpages.update({
1988        "man/ppscheck.8": "man/ppscheck.xml",
1989    })
1990
1991all_manpages = list(base_manpages.keys())
1992other_manpages = [
1993                  "man/gegps.1",
1994                  "man/xgps.1",
1995                  "man/xgpsspeed.1",
1996                  ]
1997
1998if python_manpages:
1999    all_manpages += list(python_manpages.keys())
2000
2001man_env = env.Clone()
2002if man_env.GetOption('silent'):
2003    man_env['SPAWN'] = filtered_spawn  # Suppress stderr chatter
2004manpage_targets = []
2005if manbuilder:
2006    items = list(base_manpages.items())
2007    if python_manpages:
2008        items += list(python_manpages.items())
2009
2010    for (man, xml) in items:
2011        manpage_targets.append(man_env.Man(source=xml, target=man))
2012
2013# Where it all comes together
2014
2015build = env.Alias('build',
2016                  [libraries, sbin_binaries, bin_binaries, python_targets,
2017                   "gpsd.php", manpage_targets,
2018                   "libgps.pc", "gpsd.rules"])
2019
2020if qt_env:
2021    # duplicate above?
2022    test_qgpsmm = env.Program('tests/test_qgpsmm', ['tests/test_gpsmm.cpp'],
2023                              LIBPATH=['.'],
2024                              OBJPREFIX='qt-',
2025                              LIBS=['Qgpsmm'])
2026    build_qt = qt_env.Alias('build', [compiled_qgpsmmlib, test_qgpsmm])
2027    qt_env.Default(*build_qt)
2028    testprogs.append(test_qgpsmm)
2029
2030if env['python']:
2031    build_python = python_env.Alias('build', python_targets)
2032    python_env.Default(*build_python)
2033
2034# Installation and deinstallation
2035
2036# Not here because too distro-specific: udev rules, desktop files, init scripts
2037
2038# It's deliberate that we don't install gpsd.h. It's full of internals that
2039# third-party client programs should not see.
2040headerinstall = [env.Install(installdir('includedir'), x)
2041                 for x in ("libgpsmm.h", "gps.h")]
2042
2043binaryinstall = []
2044binaryinstall.append(env.Install(installdir('sbindir'), sbin_binaries))
2045binaryinstall.append(env.Install(installdir('bindir'), bin_binaries))
2046binaryinstall.append(LibraryInstall(env, installdir('libdir'), libgps_shared,
2047                                    libgps_version))
2048# Work around a minor bug in InstallSharedLib() link handling
2049env.AddPreAction(binaryinstall, 'rm -f %s/libgps.*' % (installdir('libdir'), ))
2050
2051if qt_env:
2052    binaryinstall.append(LibraryInstall(qt_env, installdir('libdir'),
2053                         compiled_qgpsmmlib, libgps_version))
2054
2055if ((not env['debug'] and not env['profiling'] and not env['nostrip'] and
2056     not sys.platform.startswith('darwin'))):
2057    env.AddPostAction(binaryinstall, '$STRIP $TARGET')
2058
2059if env['python']:
2060    python_module_dir = str(python_libdir) + os.sep + 'gps'
2061    python_extensions_install = python_env.Install(DESTDIR + python_module_dir,
2062                                                   python_built_extensions)
2063    if ((not env['debug'] and not env['profiling'] and
2064         not env['nostrip'] and not sys.platform.startswith('darwin'))):
2065        python_env.AddPostAction(python_extensions_install, '$STRIP $TARGET')
2066
2067    python_modules_install = python_env.Install(DESTDIR + python_module_dir,
2068                                                python_modules)
2069
2070    python_progs_install = python_env.Install(installdir('bindir'),
2071                                              python_progs)
2072
2073    python_egg_info_install = python_env.Install(DESTDIR + str(python_libdir),
2074                                                 python_egg_info)
2075    python_install = [python_extensions_install,
2076                      python_modules_install,
2077                      python_progs_install,
2078                      python_egg_info_install,
2079                      # We don't need the directory explicitly for the
2080                      # install, but we do need it for the uninstall
2081                      Dir(DESTDIR + python_module_dir)]
2082
2083    # Check that Python modules compile properly
2084    python_all = python_misc + python_modules + python_progs + ['SConstruct']
2085    check_compile = []
2086    for p in python_all:
2087        # split in two lines for readability
2088        check_compile.append('cp %s tmp.py; %s -tt -m py_compile tmp.py;' %
2089                             (p, sys.executable))
2090        check_compile.append('rm tmp.py*')
2091
2092    python_compilation_regress = Utility('python-compilation-regress',
2093                                         python_all, check_compile)
2094
2095    # Sanity-check Python code.
2096    # Bletch.  We don't really want to suppress W0231 E0602 E0611 E1123,
2097    # but Python 3 syntax confuses a pylint running under Python 2.
2098    # There's an internal error in astroid that requires we disable some
2099    # auditing. This is irritating as hell but there's no help for it short
2100    # of an upstream fix.
2101    python_lint = python_misc + python_modules + python_progs + ['SConstruct']
2102
2103    pylint = Utility(
2104        "pylint", python_lint,
2105        ['''pylint --rcfile=/dev/null --dummy-variables-rgx='^_' '''
2106         '''--msg-template='''
2107         '''"{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" '''
2108         '''--reports=n --disable=F0001,C0103,C0111,C1001,C0301,C0122,C0302,'''
2109         '''C0322,C0324,C0323,C0321,C0330,C0411,C0413,E1136,R0201,R0204,'''
2110         '''R0801,'''
2111         '''R0902,R0903,R0904,R0911,R0912,R0913,R0914,R0915,W0110,W0201,'''
2112         '''W0121,W0123,W0231,W0232,W0234,W0401,W0403,W0141,W0142,W0603,'''
2113         '''W0614,W0640,W0621,W1504,E0602,E0611,E1101,E1102,E1103,E1123,'''
2114         '''F0401,I0011 ''' + " ".join(python_lint)])
2115
2116    # Additional Python readability style checks
2117    pep8 = Utility("pep8", python_lint,
2118                   ['pycodestyle --ignore=W602,E122,E241 ' +
2119                    " ".join(python_lint)])
2120
2121    flake8 = Utility("flake8", python_lint,
2122                     ['flake8 --ignore=E501,W602,E122,E241,E401 ' +
2123                      " ".join(python_lint)])
2124
2125    # get version from each python prog
2126    # this ensures they can run and gps_versions match
2127    vchk = ''
2128    verenv = env['ENV'].copy()
2129    verenv['DISPLAY'] = ''  # Avoid launching X11 in X11 progs
2130    pp = []
2131    for p in python_progs:
2132        pp.append("$PYTHON $SRCDIR/%s -V" % p)
2133    python_versions = Utility('python-versions', python_progs, pp, ENV=verenv)
2134
2135else:
2136    python_install = []
2137    python_compilation_regress = None
2138    python_versions = None
2139
2140pc_install = [env.Install(installdir('pkgconfig'), 'libgps.pc')]
2141if qt_env:
2142    pc_install.append(qt_env.Install(installdir('pkgconfig'), 'Qgpsmm.pc'))
2143    pc_install.append(qt_env.Install(installdir('libdir'), 'libQgpsmm.prl'))
2144
2145
2146maninstall = []
2147for manpage in all_manpages:
2148    if not manbuilder and not os.path.exists(manpage):
2149        continue
2150    section = manpage.split(".")[1]
2151    dest = os.path.join(installdir('mandir'), "man" + section,
2152                        os.path.basename(manpage))
2153    maninstall.append(env.InstallAs(source=manpage, target=dest))
2154install = env.Alias('install', binaryinstall + maninstall + python_install +
2155                    pc_install + headerinstall)
2156
2157
2158def Uninstall(nodes):
2159    deletes = []
2160    for node in nodes:
2161        if node.__class__ == install[0].__class__:
2162            deletes.append(Uninstall(node.sources))
2163        else:
2164            deletes.append(Delete(str(node)))
2165    return deletes
2166
2167
2168uninstall = env.Command('uninstall', '',
2169                        Flatten(Uninstall(Alias("install"))) or "")
2170env.AlwaysBuild(uninstall)
2171env.Precious(uninstall)
2172
2173# Target selection for '.' is badly broken. This is a general scons problem,
2174# not a glitch in this particular recipe. Avoid triggering the bug.
2175
2176
2177def error_action(target, source, env):
2178    raise SCons.Error.UserError("Target selection for '.' is broken.")
2179
2180
2181AlwaysBuild(Alias(".", [], error_action))
2182
2183
2184# Putting in all these -U flags speeds up cppcheck and allows it to look
2185# at configurations we actually care about.
2186Utility("cppcheck", ["gpsd.h", "packet_names.h"],
2187        "cppcheck -U__UNUSED__ -UUSE_QT -U__COVERITY__ -U__future__ "
2188        "-ULIMITED_MAX_CLIENTS -ULIMITED_MAX_DEVICES -UAF_UNSPEC -UINADDR_ANY "
2189        "-U_WIN32 -U__CYGWIN__ "
2190        "-UPATH_MAX -UHAVE_STRLCAT -UHAVE_STRLCPY -UIPTOS_LOWDELAY "
2191        "-UIPV6_TCLASS -UTCP_NODELAY -UTIOCMIWAIT --template gcc "
2192        "--enable=all --inline-suppr --suppress='*:driver_proto.c' "
2193        "--force $SRCDIR")
2194
2195# Check with clang analyzer
2196Utility("scan-build", ["gpsd.h", "packet_names.h"],
2197        "scan-build scons")
2198
2199
2200# Check the documentation for bogons, too
2201Utility("xmllint", glob.glob("man/*.xml"),
2202        "for xml in $SOURCES; do xmllint --nonet --noout --valid $$xml; done")
2203
2204# Use deheader to remove headers not required.  If the statistics line
2205# ends with other than '0 removed' there's work to be done.
2206Utility("deheader", generated_sources, [
2207    'deheader -x cpp -x contrib -x gpspacket.c '
2208    '-x monitor_proto.c -i gpsd_config.h -i gpsd.h '
2209    '-m "MORECFLAGS=\'-Werror -Wfatal-errors -DDEBUG \' scons -Q"',
2210])
2211
2212# Perform all local code-sanity checks (but not the Coverity scan).
2213audit = env.Alias('audit',
2214                  ['cppcheck',
2215                   'pylint',
2216                   'scan-build',
2217                   'valgrind-audit',
2218                   'xmllint',
2219                   ])
2220
2221#
2222# Regression tests begin here
2223#
2224# Note that the *-makeregress targets re-create the *.log.chk source
2225# files from the *.log source files.
2226
2227# Unit-test the bitfield extractor
2228bits_regress = Utility('bits-regress', [test_bits], [
2229    '$SRCDIR/tests/test_bits --quiet'
2230])
2231
2232# Unit-test the deg_to_str() converter
2233bits_regress = Utility('deg-regress', [test_gpsdclient], [
2234    '$SRCDIR/tests/test_gpsdclient'
2235])
2236
2237# Unit-test the bitfield extractor
2238matrix_regress = Utility('matrix-regress', [test_matrix], [
2239    '$SRCDIR/tests/test_matrix --quiet'
2240])
2241
2242# using regress-drivers requires socket_export being enabled.
2243if not env['socket_export'] or not env['python']:
2244    announce("GPS regression tests suppressed because socket_export "
2245             "or python is off.")
2246    gps_regress = None
2247    gpsfake_tests = None
2248else:
2249    # Regression-test the daemon.
2250    # But first dump the platform and its delay parameters.
2251    # The ":;" in this production and the later one forestalls an attempt by
2252    # SCons to install up to date versions of gpsfake and gpsctl if it can
2253    # find older versions of them in a directory on your $PATH.
2254    gps_herald = Utility('gps-herald', [gpsd, gpsctl, python_built_extensions],
2255                         ':; $PYTHON $PYTHON_COVERAGE $SRCDIR/gpsfake -T')
2256    gps_log_pattern = os.path.join('test', 'daemon', '*.log')
2257    gps_logs = glob.glob(gps_log_pattern)
2258    gps_names = [os.path.split(x)[-1][:-4] for x in gps_logs]
2259    gps_tests = []
2260    for gps_name, gps_log in zip(gps_names, gps_logs):
2261        gps_tests.append(Utility(
2262            'gps-regress-' + gps_name, gps_herald,
2263            '$SRCDIR/regress-driver -q -o -t $REGRESSOPTS ' + gps_log))
2264    gps_regress = env.Alias('gps-regress', gps_tests)
2265
2266    # Run the passthrough log in all transport modes for better coverage
2267    gpsfake_log = os.path.join('test', 'daemon', 'passthrough.log')
2268    gpsfake_tests = []
2269    for name, opts in [['pty', ''], ['udp', '-u'], ['tcp', '-o -t']]:
2270        gpsfake_tests.append(Utility('gpsfake-' + name, gps_herald,
2271                                     '$SRCDIR/regress-driver'
2272                                     ' $REGRESSOPTS -q %s %s'
2273                                     % (opts, gpsfake_log)))
2274    env.Alias('gpsfake-tests', gpsfake_tests)
2275
2276    # Build the regression tests for the daemon.
2277    # Note: You'll have to do this whenever the default leap second
2278    # changes in gpsd.h.  Many drivers rely on the default until they
2279    # get the current leap second.
2280    gps_rebuilds = []
2281    for gps_name, gps_log in zip(gps_names, gps_logs):
2282        gps_rebuilds.append(Utility('gps-makeregress-' + gps_name, gps_herald,
2283                                    '$SRCDIR/regress-driver -bq -o -t '
2284                                    '$REGRESSOPTS ' + gps_log))
2285    if GetOption('num_jobs') <= 1:
2286        Utility('gps-makeregress', gps_herald,
2287                '$SRCDIR/regress-driver -b $REGRESSOPTS %s' % gps_log_pattern)
2288    else:
2289        env.Alias('gps-makeregress', gps_rebuilds)
2290
2291# To build an individual test for a load named foo.log, put it in
2292# test/daemon and do this:
2293#    regress-driver -b test/daemon/foo.log
2294
2295# Regression-test the RTCM decoder.
2296if not env["rtcm104v2"]:
2297    announce("RTCM2 regression tests suppressed because rtcm104v2 is off.")
2298    rtcm_regress = None
2299else:
2300    rtcm_regress = Utility('rtcm-regress', [gpsdecode], [
2301        '@echo "Testing RTCM decoding..."',
2302        '@for f in $SRCDIR/test/*.rtcm2; do '
2303        '    echo "\tTesting $${f}..."; '
2304        '    TMPFILE=`mktemp -t gpsd-test.chk-XXXXXXXXXXXXXX`; '
2305        '    $SRCDIR/gpsdecode -u -j <$${f} >$${TMPFILE}; '
2306        '    diff -ub $${f}.chk $${TMPFILE} || echo "Test FAILED!"; '
2307        '    rm -f $${TMPFILE}; '
2308        'done;',
2309        '@echo "Testing idempotency of JSON dump/decode for RTCM2"',
2310        '@TMPFILE=`mktemp -t gpsd-test.chk-XXXXXXXXXXXXXX`; '
2311        '$SRCDIR/gpsdecode -u -e -j <test/synthetic-rtcm2.json >$${TMPFILE}; '
2312        '    grep -v "^#" test/synthetic-rtcm2.json | diff -ub - $${TMPFILE} '
2313        '    || echo "Test FAILED!"; '
2314        '    rm -f $${TMPFILE}; ',
2315    ])
2316
2317# Rebuild the RTCM regression tests.
2318Utility('rtcm-makeregress', [gpsdecode], [
2319    'for f in $SRCDIR/test/*.rtcm2; do '
2320    '    $SRCDIR/gpsdecode -j <$${f} >$${f}.chk; '
2321    'done'
2322])
2323
2324# Regression-test the AIVDM decoder.
2325if not env["aivdm"]:
2326    announce("AIVDM regression tests suppressed because aivdm is off.")
2327    aivdm_regress = None
2328else:
2329    # FIXME! Does not return a proper fail code
2330    aivdm_regress = Utility('aivdm-regress', [gpsdecode], [
2331        '@echo "Testing AIVDM decoding w/ CSV format..."',
2332        '@for f in $SRCDIR/test/*.aivdm; do '
2333        '    echo "\tTesting $${f}..."; '
2334        '    TMPFILE=`mktemp -t gpsd-test.chk-XXXXXXXXXXXXXX`; '
2335        '    $SRCDIR/gpsdecode -u -c <$${f} >$${TMPFILE}; '
2336        '    diff -ub $${f}.chk $${TMPFILE} || echo "Test FAILED!"; '
2337        '    rm -f $${TMPFILE}; '
2338        'done;',
2339        '@echo "Testing AIVDM decoding w/ JSON unscaled format..."',
2340        '@for f in $SRCDIR/test/*.aivdm; do '
2341        '    echo "\tTesting $${f}..."; '
2342        '    TMPFILE=`mktemp -t gpsd-test.chk-XXXXXXXXXXXXXX`; '
2343        '    $SRCDIR/gpsdecode -u -j <$${f} >$${TMPFILE}; '
2344        '    diff -ub $${f}.ju.chk $${TMPFILE} || echo "Test FAILED!"; '
2345        '    rm -f $${TMPFILE}; '
2346        'done;',
2347        '@echo "Testing AIVDM decoding w/ JSON scaled format..."',
2348        '@for f in $SRCDIR/test/*.aivdm; do '
2349        '    echo "\tTesting $${f}..."; '
2350        '    TMPFILE=`mktemp -t gpsd-test.chk-XXXXXXXXXXXXXX`; '
2351        '    $SRCDIR/gpsdecode -j <$${f} >$${TMPFILE}; '
2352        '    diff -ub $${f}.js.chk $${TMPFILE} || echo "Test FAILED!"; '
2353        '    rm -f $${TMPFILE}; '
2354        'done;',
2355        '@echo "Testing idempotency of unscaled JSON dump/decode for AIS"',
2356        '@TMPFILE=`mktemp -t gpsd-test.chk-XXXXXXXXXXXXXX`; '
2357        '$SRCDIR/gpsdecode -u -e -j <$SRCDIR/test/sample.aivdm.ju.chk '
2358        ' >$${TMPFILE}; '
2359        '    grep -v "^#" $SRCDIR/test/sample.aivdm.ju.chk '
2360        '    | diff -ub - $${TMPFILE} || echo "Test FAILED!"; '
2361        '    rm -f $${TMPFILE}; ',
2362        # Parse the unscaled json reference, dump it as scaled json,
2363        # and finally compare it with the scaled json reference
2364        '@echo "Testing idempotency of scaled JSON dump/decode for AIS"',
2365        '@TMPFILE=`mktemp -t gpsd-test.chk-XXXXXXXXXXXXXX`; '
2366        '$SRCDIR/gpsdecode -e -j <$SRCDIR/test/sample.aivdm.ju.chk '
2367        ' >$${TMPFILE};'
2368        '    grep -v "^#" $SRCDIR/test/sample.aivdm.js.chk '
2369        '    | diff -ub - $${TMPFILE} || echo "Test FAILED!"; '
2370        '    rm -f $${TMPFILE}; ',
2371    ])
2372
2373# Rebuild the AIVDM regression tests.
2374Utility('aivdm-makeregress', [gpsdecode], [
2375    'for f in $SRCDIR/test/*.aivdm; do '
2376    '    $SRCDIR/gpsdecode -u -c <$${f} > $${f}.chk; '
2377    '    $SRCDIR/gpsdecode -u -j <$${f} > $${f}.ju.chk; '
2378    '    $SRCDIR/gpsdecode -j  <$${f} > $${f}.js.chk; '
2379    'done', ])
2380
2381# Regression-test the packet getter.
2382packet_regress = UtilityWithHerald(
2383    'Testing detection of invalid packets...',
2384    'packet-regress', [test_packet], [
2385        '$SRCDIR/tests/test_packet | '
2386        ' diff -u $SRCDIR/test/packet.test.chk -', ])
2387
2388# Rebuild the packet-getter regression test
2389Utility('packet-makeregress', [test_packet], [
2390    '$SRCDIR/tests/test_packet >$SRCDIR/test/packet.test.chk', ])
2391
2392# Regression-test the geoid and variation tester.
2393geoid_regress = UtilityWithHerald(
2394    'Testing the geoid and variation models...',
2395    'geoid-regress', [test_geoid], ['$SRCDIR/tests/test_geoid'])
2396
2397# Regression-test the calendar functions
2398time_regress = Utility('time-regress', [test_mktime], [
2399    '$SRCDIR/tests/test_mktime'
2400])
2401
2402if not env['python']:
2403    unpack_regress = None
2404    misc_regress = None
2405else:
2406    # Regression test the unpacking code in libgps
2407    unpack_regress = UtilityWithHerald(
2408        'Testing the client-library sentence decoder...',
2409        'unpack-regress', [test_libgps], [
2410            '$SRCDIR/regress-driver $REGRESSOPTS -c'
2411            ' $SRCDIR/test/clientlib/*.log', ])
2412    # Unit-test the bitfield extractor
2413    misc_regress = Utility('misc-regress', [], [
2414        '{} $SRCDIR/test_clienthelpers.py'.format(target_python_path.decode()),
2415        '{} $SRCDIR/test_misc.py'.format(target_python_path.decode())
2416    ])
2417
2418
2419# Build the regression test for the sentence unpacker
2420Utility('unpack-makeregress', [test_libgps], [
2421    '@echo "Rebuilding the client sentence-unpacker tests..."',
2422    '$SRCDIR/regress-driver $REGRESSOPTS -c -b $SRCDIR/test/clientlib/*.log'
2423])
2424
2425# Unit-test the JSON parsing
2426if not env['socket_export']:
2427    json_regress = None
2428else:
2429    json_regress = Utility('json-regress', [test_json],
2430                           ['$SRCDIR/tests/test_json'])
2431
2432# Unit-test timespec math
2433timespec_regress = Utility('timespec-regress', [test_timespec], [
2434    '$SRCDIR/tests/test_timespec'
2435])
2436
2437# Unit-test float math
2438float_regress = Utility('float-regress', [test_float], [
2439    '$SRCDIR/tests/test_float'
2440])
2441
2442# Unit-test trig math
2443trig_regress = Utility('trig-regress', [test_trig], [
2444    '$SRCDIR/tests/test_trig'
2445])
2446
2447# consistency-check the driver methods
2448method_regress = UtilityWithHerald(
2449    'Consistency-checking driver methods...',
2450    'method-regress', [test_packet], [
2451        '$SRCDIR/tests/test_packet -c >/dev/null', ])
2452
2453# Test the xgps/xgpsspeed dependencies
2454if not env['python'] or not env['xgps']:
2455    test_xgps_deps = None
2456else:
2457    test_xgps_deps = UtilityWithHerald(
2458        'Testing xgps/xgpsspeed dependencies (since xgps=yes)...',
2459        'test-xgps-deps', [], [
2460            '$PYTHON $SRCDIR/test_xgps_deps.py'])
2461
2462# Run a valgrind audit on the daemon  - not in normal tests
2463valgrind_audit = Utility('valgrind-audit', [
2464    '$SRCDIR/valgrind-audit.py', python_built_extensions, gpsd],
2465    '$PYTHON $SRCDIR/valgrind-audit.py'
2466)
2467
2468# Run test builds on remote machines
2469flocktest = Utility("flocktest", [], "cd devtools; ./flocktest " + gitrepo)
2470
2471
2472# Run all normal regression tests
2473describe = UtilityWithHerald(
2474    'Run normal regression tests for %s...' % rev.strip(),
2475    'describe', [], [])
2476
2477# Delete all test programs
2478test_exes = [str(p) for p in Flatten(testprogs)]
2479test_objs = [p + '.o' for p in test_exes]
2480testclean = Utility('testclean', [],
2481                    'rm -f %s' % ' '.join(test_exes + test_objs))
2482
2483test_nondaemon = [
2484    aivdm_regress,
2485    bits_regress,
2486    describe,
2487    float_regress,
2488    geoid_regress,
2489    json_regress,
2490    matrix_regress,
2491    method_regress,
2492    misc_regress,
2493    packet_regress,
2494    python_compilation_regress,
2495    python_versions,
2496    rtcm_regress,
2497    test_xgps_deps,
2498    time_regress,
2499    timespec_regress,
2500    # trig_regress,  # not ready
2501    unpack_regress,
2502]
2503if env['socket_export']:
2504    test_nondaemon.append(test_json)
2505if env['libgpsmm']:
2506    test_nondaemon.append(test_gpsmm)
2507if qt_env:
2508    test_nondaemon.append(test_qgpsmm)
2509
2510test_quick = test_nondaemon + [gpsfake_tests]
2511test_noclean = test_quick + [gps_regress]
2512
2513env.Alias('test-nondaemon', test_nondaemon)
2514env.Alias('test-quick', test_quick)
2515check = env.Alias('check', test_noclean)
2516env.Alias('testregress', check)
2517env.Alias('build-tests', testprogs)
2518build_all = env.Alias('build-all', build + testprogs)
2519
2520# Remove all shared-memory segments.  Normally only needs to be run
2521# when a segment size changes.
2522Utility('shmclean', [], ["ipcrm  -M 0x4e545030;"
2523                         "ipcrm  -M 0x4e545031;"
2524                         "ipcrm  -M 0x4e545032;"
2525                         "ipcrm  -M 0x4e545033;"
2526                         "ipcrm  -M 0x4e545034;"
2527                         "ipcrm  -M 0x4e545035;"
2528                         "ipcrm  -M 0x4e545036;"
2529                         "ipcrm  -M 0x47505345;"
2530                         ])
2531
2532# The website directory
2533#
2534# None of these productions are fired by default.
2535# The content they handle is the GPSD website, not included in
2536# release tarballs.
2537
2538# asciidoc documents
2539if env.WhereIs('asciidoc'):
2540    adocfiles = ['AIVDM',
2541                 'client-howto',
2542                 'gpsd-time-service-howto',
2543                 'NMEA',
2544                 'ppp-howto',
2545                 'protocol-evolution',
2546                 'protocol-transition',
2547                 'time-service-intro',
2548                 ]
2549    asciidocs = ["www/" + stem + ".html" for stem in adocfiles] \
2550        + ["www/installation.html"] + ["www/README.html"]
2551    for stem in adocfiles:
2552        env.Command('www/%s.html' % stem, 'www/%s.adoc' % stem,
2553                    ['asciidoc -b html5 -a toc -o www/%s.html www/%s.adoc'
2554                     % (stem, stem)])
2555    env.Command("www/installation.html",
2556                "INSTALL.adoc",
2557                ["asciidoc -o www/installation.html INSTALL.adoc"])
2558    env.Command("www/README.html",
2559                "README.adoc",
2560                ["asciidoc -o www/README.html README.adoc"])
2561else:
2562    announce("Part of the website build requires asciidoc, not installed.")
2563    asciidocs = []
2564
2565# Non-asciidoc webpages only
2566htmlpages = Split('''
2567    www/gps2udp.html
2568    www/gpscat.html
2569    www/gpsctl.html
2570    www/gpsdctl.html
2571    www/gpsdecode.html
2572    www/gpsd.html
2573    www/gpsd_json.html
2574    www/gpsfake.html
2575    www/gps.html
2576    www/gpsinit.html
2577    www/gpsmon.html
2578    www/gpspipe.html
2579    www/gpsprof.html
2580    www/gpsrinex.html
2581    www/gpxlogger.html
2582    www/hardware.html
2583    www/internals.html
2584    www/libgps.html
2585    www/libgpsmm.html
2586    www/ntpshmmon.html
2587    www/performance/performance.html
2588    www/ppscheck.html
2589    www/replacing-nmea.html
2590    www/srec.html
2591    www/ubxtool.html
2592    www/writing-a-driver.html
2593    www/zerk.html
2594    ''')
2595
2596webpages = htmlpages + asciidocs + list(map(lambda f: f[:-3],
2597                                            glob.glob("www/*.in")))
2598
2599www = env.Alias('www', webpages)
2600
2601# Paste 'scons --quiet validation-list' to a batch validator such as
2602# http://htmlhelp.com/tools/validator/batch.html.en
2603
2604
2605def validation_list(target, source, env):
2606    for page in glob.glob("www/*.html"):
2607        if '-head' not in page:
2608            fp = open(page)
2609            if "Valid HTML" in fp.read():
2610                print(os.path.join(website, os.path.basename(page)))
2611            fp.close()
2612
2613
2614Utility("validation-list", [www], validation_list)
2615
2616# How to update the website.  Assumes a logal GitLab pages setup.
2617# See .gitlab-ci.yml
2618upload_web = Utility("website", [www],
2619                     ['rsync --exclude="*.in" -avz www/ ' +
2620                      os.environ.get('WEBSITE', '.public'),
2621                      'cp TODO NEWS ' +
2622                      os.environ.get('WEBSITE', '.public')])
2623
2624# When the URL declarations change, so must the generated web pages
2625for fn in glob.glob("www/*.in"):
2626    env.Depends(fn[:-3], "SConstruct")
2627
2628if htmlbuilder:
2629    # Manual pages
2630    for xml in glob.glob("man/*.xml"):
2631        env.HTML('www/%s.html' % os.path.basename(xml[:-4]), xml)
2632
2633    # DocBook documents
2634    for stem in ['writing-a-driver', 'performance/performance',
2635                 'replacing-nmea']:
2636        env.HTML('www/%s.html' % stem, 'www/%s.xml' % stem)
2637
2638    # The internals manual.
2639    # Doesn't capture dependencies on the subpages
2640    env.HTML('www/internals.html', '$SRCDIR/doc/internals.xml')
2641
2642# The hardware page
2643env.Command('www/hardware.html', ['gpscap.py',
2644                                  'www/hardware-head.html',
2645                                  'gpscap.ini',
2646                                  'www/hardware-tail.html'],
2647            ['(cat www/hardware-head.html && PYTHONIOENCODING=utf-8 '
2648             '$SC_PYTHON gpscap.py && cat www/hardware-tail.html) '
2649             '>www/hardware.html'])
2650
2651# The diagram editor dia is required in order to edit the diagram masters
2652Utility("www/cycle.svg", ["www/cycle.dia"],
2653        ["dia -e www/cycle.svg www/cycle.dia"])
2654
2655# Experimenting with pydoc.  Not yet fired by any other productions.
2656# scons www/ dies with this
2657
2658# # if env['python']:
2659# #     env.Alias('pydoc', "www/pydoc/index.html")
2660# #
2661# #     # We need to run epydoc with the Python version the modules built for.
2662# #     # So we define our own epydoc instead of using /usr/bin/epydoc
2663# #     EPYDOC = "python -c 'from epydoc.cli import cli; cli()'"
2664# #     env.Command('www/pydoc/index.html', python_progs + glob.glob("*.py")
2665# #                 + glob.glob("gps/*.py"), [
2666# #         'mkdir -p www/pydoc',
2667# #         EPYDOC + " -v --html --graph all -n GPSD $SOURCES -o www/pydoc",
2668# #             ])
2669
2670# Productions for setting up and performing udev tests.
2671#
2672# Requires root. Do "udev-install", then "tail -f /var/log/syslog" in
2673# another window, then run 'scons udev-test', then plug and unplug the
2674# GPS ad libitum.  All is well when you get fix reports each time a GPS
2675# is plugged in.
2676#
2677# In case you are a systemd user you might also need to watch the
2678# journalctl output. Instead of the hotplug script the gpsdctl@.service
2679# unit will handle hotplugging together with the udev rules.
2680#
2681# Note that a udev event can be triggered with an invocation like:
2682# udevadm trigger --sysname-match=ttyUSB0 --action add
2683
2684if env['systemd']:
2685    systemdinstall_target = [env.Install(DESTDIR + systemd_dir,
2686                             "systemd/%s" % (x,)) for x in
2687                             ("gpsdctl@.service", "gpsd.service",
2688                              "gpsd.socket")]
2689    systemd_install = env.Alias('systemd_install', systemdinstall_target)
2690    systemd_uninstall = env.Command(
2691        'systemd_uninstall', '',
2692        Flatten(Uninstall(Alias("systemd_install"))) or "")
2693
2694    env.AlwaysBuild(systemd_uninstall)
2695    env.Precious(systemd_uninstall)
2696    hotplug_wrapper_install = []
2697else:
2698    hotplug_wrapper_install = [
2699        'cp $SRCDIR/gpsd.hotplug ' + DESTDIR + env['udevdir'],
2700        'chmod a+x ' + DESTDIR + env['udevdir'] + '/gpsd.hotplug'
2701    ]
2702
2703udev_install = Utility('udev-install', 'install', [
2704    'mkdir -p ' + DESTDIR + env['udevdir'] + '/rules.d',
2705    'cp $SRCDIR/gpsd.rules ' + DESTDIR + env['udevdir'] +
2706    '/rules.d/25-gpsd.rules', ] + hotplug_wrapper_install)
2707
2708if env['systemd']:
2709    env.Requires(udev_install, systemd_install)
2710
2711if env['systemd'] and not env["sysroot"]:
2712    systemctl_daemon_reload = Utility('systemctl-daemon-reload', '',
2713                                      ['systemctl daemon-reload || true'])
2714    env.AlwaysBuild(systemctl_daemon_reload)
2715    env.Precious(systemctl_daemon_reload)
2716    env.Requires(systemctl_daemon_reload, systemd_install)
2717    env.Requires(udev_install, systemctl_daemon_reload)
2718
2719
2720Utility('udev-uninstall', '', [
2721    'rm -f %s/gpsd.hotplug' % env['udevdir'],
2722    'rm -f %s/rules.d/25-gpsd.rules' % env['udevdir'],
2723])
2724
2725Utility('udev-test', '', ['$SRCDIR/gpsd -N -n -F /var/run/gpsd.sock -D 5', ])
2726
2727# Cleanup
2728
2729# Dummy target for cleaning misc files
2730clean_misc = env.Alias('clean-misc')
2731# Since manpage targets are disabled in clean mode, we cover them here
2732env.Clean(clean_misc, all_manpages + other_manpages)
2733# Clean compiled Python
2734env.Clean(clean_misc,
2735          glob.glob('*.pyc') + glob.glob('gps/*.pyc') +
2736          glob.glob('gps/*.so') + ['gps/__pycache__'])
2737# Clean coverage and profiling files
2738env.Clean(clean_misc, glob.glob('*.gcno') + glob.glob('*.gcda'))
2739# Clean Python coverage files
2740env.Clean(clean_misc, glob.glob('.coverage*') + ['htmlcov/'])
2741# Clean Qt stuff
2742env.Clean(clean_misc, ['libQgpsmm.prl', 'Qgpsmm.pc'])
2743# Clean shared library files
2744env.Clean(clean_misc, glob.glob('*.so') + glob.glob('*.so.*'))
2745# Clean old location man page files
2746env.Clean(clean_misc, glob.glob('*.[0-8]'))
2747# Other misc items
2748env.Clean(clean_misc, ['config.log', 'contrib/ppscheck', 'contrib/clock_test',
2749                       'TAGS'])
2750# Clean scons state files
2751env.Clean(clean_misc, ['.sconf_temp', '.scons-option-cache', 'config.log'])
2752
2753# Default targets
2754
2755if cleaning:
2756    env.Default(build_all, audit, clean_misc)
2757    announce("You must manually remove {}".format(sconsign_file))
2758else:
2759    env.Default(build)
2760
2761# Tags for Emacs and vi
2762misc_sources = ['cgps.c',
2763                'gps2udp.c',
2764                'gpsctl.c',
2765                'gpsdctl.c',
2766                'gpsdecode.c',
2767                'gpspipe.c',
2768                'gpxlogger.c',
2769                'ntpshmmon.c',
2770                'ppscheck.c',
2771                ]
2772sources = libgpsd_sources + libgps_sources + gpsd_sources + gpsmon_sources + \
2773    misc_sources
2774env.Command('TAGS', sources, ['etags ' + " ".join(sources)])
2775
2776# Release machinery begins here
2777#
2778# We need to be in the actual project repo (i.e. not doing a -Y build)
2779# for these productions to work.
2780
2781if os.path.exists("gpsd.c") and os.path.exists(".gitignore"):
2782    distfiles = _getoutput(r"git ls-files | grep -v '^www/'").split()
2783    # for some reason distfiles is now a mix of byte strings and char strings
2784    distfiles = [polystr(d) for d in distfiles]
2785
2786    if ".gitignore" in distfiles:
2787        distfiles.remove(".gitignore")
2788    distfiles += generated_sources
2789    distfiles += all_manpages
2790    if "packaging/rpm/gpsd.spec" not in distfiles:
2791        distfiles.append("packaging/rpm/gpsd.spec")
2792
2793    # How to build a zip file.
2794    # Perversely, if the zip exists, it is modified, not replaced.
2795    # So delete it first.
2796    dozip = env.Command('zip', distfiles, [
2797        'rm -f gpsd-${VERSION}.zip',
2798        '@zip -ry gpsd-${VERSION}.zip $SOURCES -x contrib/ais-samples/\\*',
2799        '@ls -l gpsd-${VERSION}.zip',
2800    ])
2801    env.Clean(dozip, ["gpsd-${VERSION}.zip", "packaging/rpm/gpsd.spec"])
2802
2803    # How to build a tarball.
2804    # The command assume the non-portable GNU tar extension
2805    # "--transform", and will thus fail if ${TAR} is not GNU tar.
2806    # scons in theory has code to cope with this, but in practice this
2807    # is not working.  On BSD-derived systems, install GNU tar and
2808    # pass TAR=gtar in the environment.
2809    # make a .tar.gz and a .tar.xz
2810    dist = env.Command('dist', distfiles, [
2811        '@${TAR} --transform "s:^:gpsd-${VERSION}/:S" '
2812        ' -czf gpsd-${VERSION}.tar.gz --exclude contrib/ais-samples $SOURCES',
2813        '@${TAR} --transform "s:^:gpsd-${VERSION}/:S" '
2814        ' -cJf gpsd-${VERSION}.tar.xz --exclude contrib/ais-samples $SOURCES',
2815        '@ls -l gpsd-${VERSION}.tar.gz',
2816    ])
2817    env.Clean(dist, ["gpsd-${VERSION}.tar.gz", "packaging/rpm/gpsd.spec"])
2818
2819    # Make RPM from the specfile in packaging
2820    Utility('dist-rpm', dist, 'rpmbuild -ta gpsd-${VERSION}.tar.gz')
2821
2822    # Make sure build-from-tarball works.
2823    testbuild = Utility('testbuild', [dist], [
2824        '${TAR} -xzvf gpsd-${VERSION}.tar.gz',
2825        'cd gpsd-${VERSION}; scons',
2826        'rm -fr gpsd-${VERSION}',
2827    ])
2828
2829    releasecheck = env.Alias('releasecheck', [
2830        testbuild,
2831        check,
2832        audit,
2833        flocktest,
2834    ])
2835
2836    # The chmod copes with the fact that scp will give a
2837    # replacement the permissions of the *original*...
2838    upload_release = Utility('upload-release', [dist], [
2839        'rm -f gpsd-*tar*sig',
2840        'gpg -b gpsd-${VERSION}.tar.gz',
2841        'gpg -b gpsd-${VERSION}.tar.xz',
2842        'chmod ug=rw,o=r gpsd-${VERSION}.tar.*',
2843        'scp gpsd-${VERSION}.tar.* ' + scpupload,
2844    ])
2845
2846    # How to tag a release
2847    tag_release = Utility('tag-release', [], [
2848        'git tag -s -m "Tagged for external release ${VERSION}" \
2849         release-${VERSION}'])
2850    upload_tags = Utility('upload-tags', [], ['git push --tags'])
2851
2852    # Local release preparation. This production will require Internet access,
2853    # but it doesn't do any uploads or public repo mods.
2854    #
2855    # Note that tag_release has to fire early, otherwise the value of REVISION
2856    # won't be right when revision.h is generated for the tarball.
2857    releaseprep = env.Alias("releaseprep",
2858                            [Utility("distclean", [], ["rm -f revision.h"]),
2859                             tag_release,
2860                             dist])
2861    # Undo local release preparation
2862    Utility("undoprep", [], ['rm -f gpsd-${VERSION}.tar.gz;',
2863                             'git tag -d release-${VERSION};'])
2864
2865    # All a buildup to this.
2866    env.Alias("release", [releaseprep,
2867                          upload_release,
2868                          upload_tags,
2869                          upload_web])
2870
2871    # Experimental release mechanics using shipper
2872    # This will ship a freecode metadata update
2873    Utility("ship", [dist, "control"],
2874            ['shipper version=%s | sh -e -x' % gpsd_version])
2875
2876# The following sets edit modes for GNU EMACS
2877# Local Variables:
2878# mode:python
2879# End:
2880# vim: set expandtab shiftwidth=4
2881