Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/tools/ports/__init__.py
4130 views
1
# Copyright 2014 The Emscripten Authors. All rights reserved.
2
# Emscripten is available under two separate licenses, the MIT license and the
3
# University of Illinois/NCSA Open Source License. Both these licenses can be
4
# found in the LICENSE file.
5
6
import logging
7
import hashlib
8
import os
9
from pathlib import Path
10
import shutil
11
import glob
12
import importlib.util
13
from inspect import signature
14
import sys
15
import subprocess
16
from typing import Set, Dict
17
from urllib.request import urlopen
18
19
from tools import cache
20
from tools import config
21
from tools import shared
22
from tools import system_libs
23
from tools import utils
24
from tools.settings import settings
25
from tools.toolchain_profiler import ToolchainProfiler
26
27
ports = []
28
29
ports_by_name: Dict[str, object] = {}
30
31
ports_needed = set()
32
33
# Variant builds that we want to support for certain ports
34
# {variant_name: (port_name, extra_settings)}
35
port_variants = {}
36
37
ports_dir = os.path.dirname(os.path.abspath(__file__))
38
39
logger = logging.getLogger('ports')
40
41
42
def get_port_by_name(name):
43
port = ports_by_name[name]
44
if port.is_external:
45
load_external_port(port)
46
return ports_by_name[name]
47
else:
48
return port
49
50
51
def init_port(name, port):
52
ports.append(port)
53
port.is_contrib = name.startswith('contrib.')
54
port.is_external = hasattr(port, 'EXTERNAL_PORT')
55
port.name = name
56
ports_by_name[port.name] = port
57
if port.is_external:
58
init_external_port(name, port)
59
else:
60
init_local_port(name, port)
61
62
63
def init_local_port(name, port):
64
if not hasattr(port, 'needed'):
65
port.needed = lambda s: name in ports_needed
66
else:
67
needed = port.needed
68
port.needed = lambda s: needed(s) or name in ports_needed
69
if not hasattr(port, 'process_dependencies'):
70
port.process_dependencies = lambda x: 0
71
if not hasattr(port, 'linker_setup'):
72
port.linker_setup = lambda x, y: 0
73
if not hasattr(port, 'deps'):
74
port.deps = []
75
if not hasattr(port, 'process_args'):
76
port.process_args = lambda x: []
77
if not hasattr(port, 'variants'):
78
# port variants (default: no variants)
79
port.variants = {}
80
if not hasattr(port, 'show'):
81
port.show = lambda: f'{port.name} (--use-port={port.name}; {port.LICENSE})'
82
83
for variant, extra_settings in port.variants.items():
84
if variant in port_variants:
85
utils.exit_with_error('duplicate port variant: `%s`' % variant)
86
port_variants[variant] = (port.name, extra_settings)
87
88
validate_port(port)
89
90
91
def load_port_module(module_name, port_file):
92
spec = importlib.util.spec_from_file_location(module_name, port_file)
93
port = importlib.util.module_from_spec(spec)
94
spec.loader.exec_module(port)
95
return port
96
97
98
def load_external_port(external_port):
99
name = external_port.name
100
up_to_date = Ports.fetch_port_artifact(name, external_port.EXTERNAL_PORT, external_port.SHA512)
101
port_file = os.path.join(Ports.get_dir(), name, external_port.PORT_FILE)
102
local_port = load_port_module(f'tools.ports.external.{name}', port_file)
103
ports.remove(external_port)
104
for a in ['URL', 'DESCRIPTION', 'LICENSE']:
105
if not hasattr(local_port, a):
106
setattr(local_port, a, getattr(external_port, a))
107
init_port(name, local_port)
108
if not up_to_date:
109
Ports.clear_project_build(name)
110
111
112
def init_external_port(name, port):
113
expected_attrs = ['SHA512', 'PORT_FILE', 'URL', 'DESCRIPTION', 'LICENSE']
114
for a in expected_attrs:
115
assert hasattr(port, a), 'port %s is missing %s' % (port, a)
116
port.needed = lambda s: name in ports_needed
117
port.show = lambda: f'{port.name} (--use-port={port.name}; {port.LICENSE})'
118
119
120
def load_port(path, name=None):
121
if not name:
122
name = shared.unsuffixed_basename(path)
123
if name in ports_by_name:
124
utils.exit_with_error(f'port path [`{path}`] is invalid: duplicate port name `{name}`')
125
port = load_port_module(f'tools.ports.{name}', path)
126
init_port(name, port)
127
return name
128
129
130
def validate_port(port):
131
expected_attrs = ['get', 'clear', 'show']
132
if port.is_contrib:
133
expected_attrs += ['URL', 'DESCRIPTION', 'LICENSE']
134
if hasattr(port, 'handle_options'):
135
expected_attrs += ['OPTIONS']
136
for a in expected_attrs:
137
assert hasattr(port, a), 'port %s is missing %s' % (port, a)
138
139
140
@ToolchainProfiler.profile()
141
def read_ports():
142
for filename in os.listdir(ports_dir):
143
if not filename.endswith('.py') or filename == '__init__.py':
144
continue
145
load_port(os.path.join(ports_dir, filename))
146
147
contrib_dir = os.path.join(ports_dir, 'contrib')
148
for filename in os.listdir(contrib_dir):
149
if not filename.endswith('.py') or filename == '__init__.py':
150
continue
151
name = 'contrib.' + shared.unsuffixed(filename)
152
load_port(os.path.join(contrib_dir, filename), name)
153
154
155
def get_all_files_under(dirname):
156
for path, _, files in os.walk(dirname):
157
for name in files:
158
yield os.path.join(path, name)
159
160
161
def dir_is_newer(dir_a, dir_b):
162
assert os.path.exists(dir_a)
163
assert os.path.exists(dir_b)
164
files_a = ((x, os.path.getmtime(x)) for x in get_all_files_under(dir_a))
165
files_b = ((x, os.path.getmtime(x)) for x in get_all_files_under(dir_b))
166
newest_a = max(files_a, key=lambda f: f[1])
167
newest_b = max(files_b, key=lambda f: f[1])
168
logger.debug('newest_a: %s %s', *newest_a)
169
logger.debug('newest_b: %s %s', *newest_b)
170
return newest_a[1] > newest_b[1]
171
172
173
def maybe_copy(src, dest):
174
"""Just like shutil.copyfile, but will do nothing if the destination already
175
exists and has the same contents as the source.
176
177
In the case where a library is built in multiple different configurations,
178
we want to avoids racing between processes that are reading headers (without
179
holding the cache lock) (e.g. normal compile steps) and a process that is
180
building/installing a new flavor of a given library. In this case the
181
headers will be "re-installed" but we skip the actual filesystem mods
182
to avoid racing with other processes that might be reading these files.
183
"""
184
if os.path.exists(dest) and utils.read_binary(src) == utils.read_binary(dest):
185
return
186
shutil.copyfile(src, dest)
187
188
189
class Ports:
190
"""emscripten-ports library management (https://github.com/emscripten-ports).
191
"""
192
193
@staticmethod
194
def get_include_dir(*parts):
195
dirname = cache.get_include_dir(*parts)
196
shared.safe_ensure_dirs(dirname)
197
return dirname
198
199
@staticmethod
200
def install_header_dir(src_dir, target=None):
201
"""Like install_headers but recursively copied all files in a directory"""
202
if not target:
203
target = os.path.basename(src_dir)
204
dest = Ports.get_include_dir(target)
205
logger.debug(f'installing headers: {dest}')
206
shutil.copytree(src_dir, dest, dirs_exist_ok=True, copy_function=maybe_copy)
207
208
@staticmethod
209
def install_file(filename, target):
210
sysroot = cache.get_sysroot_dir()
211
target_dir = os.path.join(sysroot, os.path.dirname(target))
212
os.makedirs(target_dir, exist_ok=True)
213
maybe_copy(filename, os.path.join(sysroot, target))
214
215
@staticmethod
216
def install_headers(src_dir, pattern='*.h', target=None):
217
logger.debug('install_headers')
218
dest = Ports.get_include_dir()
219
assert os.path.exists(dest)
220
if target:
221
dest = os.path.join(dest, target)
222
shared.safe_ensure_dirs(dest)
223
matches = glob.glob(os.path.join(src_dir, pattern))
224
assert matches, f'no headers found to install in {src_dir}'
225
for f in matches:
226
logger.debug('installing: ' + os.path.join(dest, os.path.basename(f)))
227
maybe_copy(f, os.path.join(dest, os.path.basename(f)))
228
229
@staticmethod
230
def build_port(src_dir, output_path, port_name, includes=[], flags=[], cxxflags=[], exclude_files=[], exclude_dirs=[], srcs=[]): # noqa
231
mangled_name = str(Path(output_path).relative_to(Path(cache.get_sysroot(True)) / 'lib'))
232
mangled_name = mangled_name.replace(os.sep, '_').replace('.a', '').replace('-emscripten', '')
233
build_dir = os.path.join(Ports.get_build_dir(), port_name, mangled_name)
234
logger.debug(f'build_port: {port_name} {output_path} in {build_dir}')
235
if srcs:
236
srcs = [os.path.join(src_dir, s) for s in srcs]
237
else:
238
srcs = []
239
for root, dirs, files in os.walk(src_dir):
240
for ex in exclude_dirs:
241
if ex in dirs:
242
dirs.remove(ex)
243
for f in files:
244
ext = shared.suffix(f)
245
if ext in ('.c', '.cpp') and not any((excluded in f) for excluded in exclude_files):
246
srcs.append(os.path.join(root, f))
247
248
cflags = system_libs.get_base_cflags(build_dir) + ['-O2', '-I' + src_dir] + flags
249
for include in includes:
250
cflags.append('-I' + include)
251
252
if system_libs.USE_NINJA:
253
os.makedirs(build_dir, exist_ok=True)
254
ninja_file = os.path.join(build_dir, 'build.ninja')
255
system_libs.ensure_sysroot()
256
system_libs.create_ninja_file(srcs, ninja_file, output_path, cflags=cflags)
257
if not os.getenv('EMBUILDER_PORT_BUILD_DEFERRED'):
258
system_libs.run_ninja(build_dir)
259
else:
260
commands = []
261
objects = []
262
for src in srcs:
263
relpath = os.path.relpath(src, src_dir)
264
obj = os.path.join(build_dir, relpath) + '.o'
265
dirname = os.path.dirname(obj)
266
os.makedirs(dirname, exist_ok=True)
267
cmd = [shared.EMCC, '-c', src, '-o', obj] + cflags
268
if shared.suffix(src) in ('.cc', '.cxx', '.cpp'):
269
cmd[0] = shared.EMXX
270
cmd += cxxflags
271
commands.append(cmd)
272
objects.append(obj)
273
274
system_libs.run_build_commands(commands, num_inputs=len(srcs))
275
system_libs.create_lib(output_path, objects)
276
277
return output_path
278
279
@staticmethod
280
def get_dir(*parts):
281
dirname = os.path.join(config.PORTS, *parts)
282
shared.safe_ensure_dirs(dirname)
283
return dirname
284
285
@staticmethod
286
def erase():
287
dirname = Ports.get_dir()
288
utils.delete_dir(dirname)
289
290
@staticmethod
291
def get_build_dir():
292
return system_libs.get_build_dir()
293
294
name_cache: Set[str] = set()
295
296
@staticmethod
297
def fetch_port_artifact(name, url, sha512hash=None):
298
"""This function only fetches the port and returns True when the port is up to date, False otherwise"""
299
# To compute the sha512 hash, run `curl URL | sha512sum`.
300
fullname = Ports.get_dir(name)
301
302
if name not in Ports.name_cache: # only mention each port once in log
303
logger.debug(f'including port: {name}')
304
logger.debug(f' (at {fullname})')
305
Ports.name_cache.add(name)
306
307
# EMCC_LOCAL_PORTS: A hacky way to use a local directory for a port. This
308
# is not tested but can be useful for debugging
309
# changes to a port.
310
#
311
# if EMCC_LOCAL_PORTS is set, we use a local directory as our ports. This is useful
312
# for testing. This env var should be in format
313
# name=dir,name=dir
314
# e.g.
315
# sdl2=/home/username/dev/ports/SDL2
316
# so you could run
317
# EMCC_LOCAL_PORTS="sdl2=/home/alon/Dev/ports/SDL2" ./test/runner.py browser.test_sdl2_mouse
318
# this will simply copy that directory into the ports directory for sdl2, and use that. It also
319
# clears the build, so that it is rebuilt from that source.
320
local_ports = os.environ.get('EMCC_LOCAL_PORTS')
321
if local_ports:
322
logger.warning('using local ports: %s' % local_ports)
323
local_ports = [pair.split('=', 1) for pair in local_ports.split(',')]
324
for local_name, path in local_ports:
325
if name == local_name:
326
port = ports_by_name.get(name)
327
if not port:
328
utils.exit_with_error('%s is not a known port' % name)
329
if not hasattr(port, 'SUBDIR'):
330
utils.exit_with_error(f'port {name} lacks .SUBDIR attribute, which we need in order to override it locally, please update it')
331
subdir = port.SUBDIR
332
target = os.path.join(fullname, subdir)
333
334
uptodate_message = f'not grabbing local port: {name} from {path} to {fullname} (subdir: {subdir}) as the destination {target} is newer (run emcc --clear-ports if that is incorrect)'
335
# before acquiring the lock we have an early out if the port already exists
336
if os.path.exists(target) and dir_is_newer(path, target):
337
logger.warning(uptodate_message)
338
return True
339
with cache.lock('unpack local port'):
340
# Another early out in case another process unpackage the library while we were
341
# waiting for the lock
342
if os.path.exists(target) and not dir_is_newer(path, target):
343
logger.warning(uptodate_message)
344
return True
345
logger.warning(f'grabbing local port: {name} from {path} to {fullname} (subdir: {subdir})')
346
utils.delete_dir(fullname)
347
shutil.copytree(path, target)
348
return False
349
350
url_filename = url.rsplit('/')[-1]
351
ext = url_filename.split('.', 1)[1]
352
fullpath = fullname + '.' + ext
353
354
def retrieve():
355
# retrieve from remote server
356
logger.info(f'retrieving port: {name} from {url}')
357
358
if utils.MACOS:
359
# Use `curl` over `urllib` on macOS to avoid issues with
360
# certificate verification.
361
# https://stackoverflow.com/questions/40684543/how-to-make-python-use-ca-certificates-from-mac-os-truststore
362
# Unlike on Windows or Linux, curl is guaranteed to always be
363
# available on macOS.
364
data = subprocess.check_output(['curl', '-sSL', url])
365
else:
366
f = urlopen(url)
367
data = f.read()
368
369
if sha512hash:
370
actual_hash = hashlib.sha512(data).hexdigest()
371
if actual_hash != sha512hash:
372
utils.exit_with_error(f'Unexpected hash: {actual_hash}\n'
373
'If you are updating the port, please update the hash.')
374
utils.write_binary(fullpath, data)
375
376
marker = os.path.join(fullname, '.emscripten_url')
377
378
def unpack():
379
logger.info(f'unpacking port: {name}')
380
shared.safe_ensure_dirs(fullname)
381
shutil.unpack_archive(filename=fullpath, extract_dir=fullname)
382
utils.write_file(marker, url + '\n')
383
384
def up_to_date():
385
return os.path.exists(marker) and utils.read_file(marker).strip() == url
386
387
# before acquiring the lock we have an early out if the port already exists
388
if up_to_date():
389
return True
390
391
# main logic. do this under a cache lock, since we don't want multiple jobs to
392
# retrieve the same port at once
393
cache.ensure() # TODO: find a better place for this (necessary at the moment)
394
with cache.lock('unpack port'):
395
if os.path.exists(fullpath):
396
# Another early out in case another process unpackage the library while we were
397
# waiting for the lock
398
if up_to_date():
399
return True
400
# file exists but tag is bad
401
logger.warning('local copy of port is not correct, retrieving from remote server')
402
utils.delete_dir(fullname)
403
utils.delete_file(fullpath)
404
405
retrieve()
406
unpack()
407
408
return False
409
410
@staticmethod
411
def fetch_project(name, url, sha512hash=None):
412
if not Ports.fetch_port_artifact(name, url, sha512hash):
413
# we unpacked a new version, clear the build in the cache
414
Ports.clear_project_build(name)
415
416
@staticmethod
417
def clear_project_build(name):
418
port = get_port_by_name(name)
419
port.clear(Ports, settings, shared)
420
build_dir = os.path.join(Ports.get_build_dir(), name)
421
logger.debug(f'clearing port build: {name} {build_dir}')
422
utils.delete_dir(build_dir)
423
return build_dir
424
425
@staticmethod
426
def write_file(filename, contents):
427
if os.path.exists(filename) and utils.read_file(filename) == contents:
428
return
429
utils.write_file(filename, contents)
430
431
@staticmethod
432
def make_pkg_config(name, version, flags):
433
pkgconfig_dir = cache.get_sysroot_dir('lib/pkgconfig')
434
filename = os.path.join(pkgconfig_dir, name + '.pc')
435
Ports.write_file(filename, f'''
436
Name: {name}
437
Description: {name} port from emscripten
438
Version: {version}
439
Libs: {flags}
440
Cflags: {flags}
441
''')
442
443
444
class OrderedSet:
445
"""Partial implementation of OrderedSet. Just enough for what we need here."""
446
def __init__(self, items):
447
self.dict = {}
448
for i in items:
449
self.dict[i] = True
450
451
def __repr__(self):
452
return f"OrderedSet({list(self.dict.keys())})"
453
454
def __len__(self):
455
return len(self.dict.keys())
456
457
def copy(self):
458
return OrderedSet(self.dict.keys())
459
460
def __iter__(self):
461
return iter(self.dict.keys())
462
463
def pop(self, index=-1):
464
key = list(self.dict.keys())[index]
465
self.dict.pop(key)
466
return key
467
468
def add(self, item):
469
self.dict[item] = True
470
471
def remove(self, item):
472
del self.dict[item]
473
474
475
def dependency_order(port_list):
476
# Perform topological sort of ports according to the dependency DAG
477
port_map = {p.name: p for p in port_list}
478
479
# Perform depth first search of dependency graph adding nodes to
480
# the stack only after all children have been explored.
481
stack = []
482
unsorted = OrderedSet(port_list)
483
484
def dfs(node):
485
for dep in node.deps:
486
dep, _ = split_port_options(dep)
487
child = port_map[dep]
488
if child in unsorted:
489
unsorted.remove(child)
490
dfs(child)
491
stack.append(node)
492
493
while unsorted:
494
dfs(unsorted.pop())
495
496
return stack
497
498
499
def resolve_dependencies(port_set, settings, cflags_only=False):
500
def add_deps(node):
501
sig = signature(node.process_dependencies)
502
if len(sig.parameters) == 2:
503
# The optional second parameter here is useful for ports that want
504
# to mutate linker-only settings. Modifying these settings during the
505
# compile phase (or in a compile-only) command generates errors.
506
node.process_dependencies(settings, cflags_only)
507
else:
508
node.process_dependencies(settings)
509
for d in node.deps:
510
d, _ = split_port_options(d)
511
if d not in ports_by_name:
512
utils.exit_with_error(f'unknown dependency `{d}` for port `{node.name}`')
513
dep = get_port_by_name(d)
514
if dep not in port_set:
515
port_set.add(dep)
516
add_deps(dep)
517
518
for port in port_set.copy():
519
add_deps(port)
520
521
522
def handle_use_port_error(arg, message):
523
utils.exit_with_error(f'error with `--use-port={arg}` | {message}')
524
525
526
def show_port_help_and_exit(port):
527
print(port.show())
528
if hasattr(port, 'DESCRIPTION'):
529
print(port.DESCRIPTION)
530
if hasattr(port, 'OPTIONS'):
531
print("Options:")
532
for option, desc in port.OPTIONS.items():
533
print(f'* {option}: {desc}')
534
else:
535
print("No options.")
536
if hasattr(port, 'URL'):
537
print(f'More info: {port.URL}')
538
sys.exit(0)
539
540
541
# extract dict and delegate to port.handle_options for handling (format is 'option1=value1:option2=value2')
542
def handle_port_options(name, options, error_handler):
543
port = get_port_by_name(name)
544
if options == 'help':
545
show_port_help_and_exit(port)
546
if not hasattr(port, 'handle_options'):
547
error_handler(f'no options available for port `{name}`')
548
else:
549
options_dict = {}
550
for name_value in options.replace('::', '\0').split(':'):
551
name_value = name_value.replace('\0', ':')
552
nv = name_value.split('=', 1)
553
if len(nv) != 2:
554
error_handler(f'`{name_value}` is missing a value')
555
if nv[0] not in port.OPTIONS:
556
error_handler(f'`{nv[0]}` is not supported; available options are {port.OPTIONS}')
557
if nv[0] in options_dict:
558
error_handler(f'duplicate option `{nv[0]}`')
559
options_dict[nv[0]] = nv[1]
560
port.handle_options(options_dict, error_handler)
561
562
563
# handle port dependencies (ex: deps=['sdl2_image:formats=jpg'])
564
def handle_port_deps(name, error_handler):
565
port = get_port_by_name(name)
566
for dep in port.deps:
567
dep_name, dep_options = split_port_options(dep)
568
if dep_name not in ports_by_name:
569
error_handler(f'unknown dependency `{dep_name}`')
570
if dep_options:
571
handle_port_options(dep_name, dep_options, error_handler)
572
handle_port_deps(dep_name, error_handler)
573
574
575
def split_port_options(arg):
576
# Ignore ':' in first or second char of string since we could be dealing with a windows drive separator
577
pos = arg.find(':', 2)
578
if pos != -1:
579
return arg[:pos], arg[pos + 1:]
580
else:
581
return arg, None
582
583
584
def handle_use_port_arg(settings, arg, error_handler=None):
585
if not error_handler:
586
def error_handler(message):
587
handle_use_port_error(arg, message)
588
name, options = split_port_options(arg)
589
if name.endswith('.py'):
590
port_file_path = name
591
if not os.path.isfile(port_file_path):
592
error_handler(f'not a valid port path: {port_file_path}')
593
name = load_port(port_file_path)
594
elif name not in ports_by_name:
595
error_handler(f'invalid port name: `{name}`')
596
ports_needed.add(name)
597
if options:
598
handle_port_options(name, options, error_handler)
599
handle_port_deps(name, error_handler)
600
return name
601
602
603
def get_needed_ports(settings, cflags_only=False):
604
# Start with directly needed ports, and transitively add dependencies
605
needed = OrderedSet(get_port_by_name(p.name) for p in ports if p.needed(settings))
606
resolve_dependencies(needed, settings, cflags_only)
607
return needed
608
609
610
def build_port(port_name, settings):
611
port = get_port_by_name(port_name)
612
port_set = OrderedSet([port])
613
resolve_dependencies(port_set, settings)
614
for port in dependency_order(port_set):
615
port.get(Ports, settings, shared)
616
617
618
def clear_port(port_name, settings):
619
port = get_port_by_name(port_name)
620
port.clear(Ports, settings, shared)
621
622
623
def clear():
624
Ports.erase()
625
626
627
def get_libs(settings):
628
"""Called add link time to calculate the list of port libraries.
629
Can have the side effect of building and installing the needed ports.
630
"""
631
ret = []
632
needed = get_needed_ports(settings)
633
634
for port in dependency_order(needed):
635
port.linker_setup(Ports, settings)
636
# port.get returns a list of libraries to link
637
ret += port.get(Ports, settings, shared)
638
639
ret.reverse()
640
return ret
641
642
643
def add_cflags(args, settings): # noqa: U100
644
"""Called during compile phase add any compiler flags (e.g -Ifoo) needed
645
by the selected ports. Can also add/change settings.
646
647
Can have the side effect of building and installing the needed ports.
648
"""
649
650
# Legacy SDL1 port is not actually a port at all but builtin
651
if settings.USE_SDL == 1:
652
args += ['-I' + Ports.get_include_dir('SDL')]
653
654
needed = get_needed_ports(settings, cflags_only=True)
655
656
# Now get (i.e. build) the ports in dependency order. This is important because the
657
# headers from one port might be needed before we can build the next.
658
for port in dependency_order(needed):
659
# When using embuilder, don't build the dependencies
660
if not os.getenv('EMBUILDER_PORT_BUILD_DEFERRED'):
661
port.get(Ports, settings, shared)
662
args += port.process_args(Ports)
663
664
665
def show_ports():
666
sorted_ports = sorted(ports, key=lambda p: p.name)
667
print('Available official ports:')
668
for port in sorted_ports:
669
if not port.is_contrib:
670
print(' ', port.show())
671
print('Available contrib ports:')
672
for port in sorted_ports:
673
if port.is_contrib:
674
print(' ', port.show())
675
676
677
read_ports()
678
679