#!/usr/bin/env python3

import os
import shutil
import sys
import platform
import subprocess
import tempfile
import hashlib
import re

def sha512(s):
  h = hashlib.sha512()
  h.update(s)
  return h.digest()

prefix = '/usr/local'
clean = True
trim = True
tryifunc = True
valgrind = True
darwin = platform.system() == 'Darwin'

def hostsanitize(host):
  host = host.split('-')[0]
  host = ''.join(c for c in host if c in '_0123456789abcdefghijklmnopqrstuvwxyz')
  for prefix,result in (
    ('amd64','amd64'), ('x86_64','amd64'),
    ('x86','x86'), ('i386','x86'), ('i486','x86'), ('i586','x86'), ('i686','x86'),
    ('arm64','arm64'), ('armv8','arm64'), ('aarch64','arm64'),
    ('arm','arm32'),
    ('riscv64','riscv64'),
    ('riscv','riscv32'),
    ('mips64big','mips64big'),
    ('mipsn32big','mipsn32big'),
    ('mips32big','mips32big'),
    ('mips64','mips64little'),
    ('mipsn32','mipsn32little'),
    ('mips32','mips32little'),
    ('ppc64little','ppc64little'), ('ppc64le','ppc64little'),
    ('ppc64','ppc64big'), ('powerpc64','ppc64big'),
    ('powerpc','ppc32big'), ('ppc','ppc32big'),
    ('sparc64','sparc64'), ('sparcv9','sparc64'), ('sun4u','sparc64'), ('sun4v','sparc64'),
    ('sparc','sparc32'), ('sun','sparc32'),
  ):
    if host.startswith(prefix): return result
  return host

host = hostsanitize(platform.machine())

configurelog = ''

def log(x):
  global configurelog
  configurelog += x+'\n'
  print(x)

makefile = ''

for arg in sys.argv[1:]:
  if arg.startswith('--prefix='):
    prefix = arg[9:]
    continue
  if arg.startswith('--host='):
    host = hostsanitize(arg[7:])
    continue
  if arg == '--clean':
    clean = True
    continue
  if arg == '--no-clean':
    clean = False
    continue
  if arg == '--trim':
    trim = True
    continue
  if arg == '--no-trim':
    trim = False
    continue
  if arg == '--tryifunc':
    tryifunc = True
    continue
  if arg == '--no-tryifunc':
    tryifunc = False
    continue
  if arg == '--darwin':
    darwin = True
    continue
  if arg == '--no-darwin':
    darwin = False
    continue
  if arg == '--valgrind':
    valgrind = True
    continue
  if arg == '--no-valgrind':
    valgrind = False
    continue
  raise ValueError('unrecognized argument %s' % arg)

echoargs = './configure'
echoargs += ' --prefix=%s' % prefix
echoargs += ' --host=%s' % host
if clean: echoargs += ' --clean'
if not clean: echoargs += ' --no-clean'
if trim: echoargs += ' --trim'
if not trim: echoargs += ' --no-trim'
if tryifunc: echoargs += ' --tryifunc'
if not tryifunc: echoargs += ' --no-tryifunc'
if darwin: echoargs += ' --darwin'
if not darwin: echoargs += ' --no-darwin'
if valgrind: echoargs += ' --valgrind'
if not valgrind: echoargs += ' --no-valgrind'
log(echoargs)

if prefix[0] != '/':
  raise ValueError('prefix %s is not an absolute path' % prefix)

if clean:
  shutil.rmtree('build/%s' % host,ignore_errors=True)

def dirlinksym(dir,source,target):
  with tempfile.TemporaryDirectory(dir=dir) as t:
    os.symlink(target,'%s/symlink' % t)
    os.rename('%s/symlink' % t,'%s/%s' % (dir,source))

os.makedirs('build/%s' % host,exist_ok=True)
os.makedirs('build/%s/package/bin' % host,exist_ok=True)
os.makedirs('build/%s/package/lib' % host,exist_ok=True)
os.makedirs('build/%s/package/include' % host,exist_ok=True)

if clean:
  os.symlink('../..','build/%s/src' % host)

def copytree(src,dst,acceptfn=None):
  # starting with python 3.8 can use shutil.copytree
  # with dirs_exist_ok=True
  # but want to support older versions of python too
  # and want to symlink rather than copying

  os.makedirs(dst,exist_ok=True)
  for fn in sorted(os.listdir(src)):
    srcfn = '%s/%s' % (src,fn)
    if os.path.isdir(srcfn):
      dstfn = '%s/%s' % (dst,fn)
      copytree(srcfn,dstfn)
    else:
      if acceptfn is not None:
        if not acceptfn(fn): continue
      dirlinksym(dst,fn,'../'*(len(dst.split('/'))-2)+'src/'+srcfn)
  shutil.copystat(src,dst)

def acceptfn_shared(fn):
  if fn.startswith('shared-'): return True
  if fn.endswith('.S'): return True
  if fn.endswith('.h'): return True
  if fn.endswith('.data'): return True
  return False

def acceptfn_nonshared(fn):
  if fn.startswith('shared-'):
    if fn.endswith('.c'): return False
    return True
  if not fn.endswith('.S'): return True
  return False

shutil.copy2('api','build/%s/api' % host)
copytree('scripts-build','build/%s/scripts' % host)
copytree('cpuid','build/%s/cpuid' % host)
copytree('priority','build/%s/priority' % host)
if valgrind:
  copytree('include-build-valgrind','build/%s/include-build' % host)
else:
  copytree('include-build','build/%s/include-build' % host)

with open('project/namespace') as f:
  projectnamespace = f.read().strip()
with open('project/library') as f:
  projectlibrary = f.read().strip()

# ----- shared-library variations

so = 'dylib' if darwin else 'so'
so1 = '1.dylib' if darwin else 'so.1'
soname = 'install_name' if darwin else 'soname'
sharedlibopts = '-shared -flat_namespace' if darwin else '-shared'
with open(f'build/{host}/scripts/sharedlib-so1','w') as f:
  f.write(so1+'\n')
with open(f'build/{host}/scripts/sharedlib-soname','w') as f:
  f.write(soname+'\n')
with open(f'build/{host}/scripts/sharedlib-opts','w') as f:
  f.write(sharedlibopts+'\n')

# ----- cryptoint

os.makedirs(f'build/{host}/cryptoint',exist_ok=True)
dirlinksym(f'build/{host}/cryptoint','include-build','../include-build')

cryptoint_dep = []

def cryptoint_h(fn):
  if 'N' in fn: return False
  return fn.endswith('.h')

def cryptoint_c(fn):
  global makefile
  global cryptoint_dep
  if 'N' in fn: return False
  if not fn.endswith('.c'): return False
  basename = fn[:-2]
  obj = basename+'.o'
  M = '\n'
  M += f'cryptoint/{obj}: cryptoint/{fn}\n'
  M += f'\tscripts/compiledefault cryptoint {basename} c -I include-build\n'
  makefile += M
  cryptoint_dep += [f'cryptoint/{obj}']
  return True

copytree('cryptoint',f'build/{host}/include-build',cryptoint_h)
copytree('cryptoint',f'build/{host}/cryptoint',cryptoint_c)

M = '\n'
M += 'cryptoint-all: \\\n%s\n' % '\\\n'.join(cryptoint_dep)
M += '\ttouch cryptoint-all\n'
makefile += M

# ----- compilers

def compilerversion(c):
  try:
    p = subprocess.Popen(c.split()+['--version'],stdout=subprocess.PIPE,stderr=subprocess.STDOUT,universal_newlines=True)
    out,err = p.communicate()
    assert not err
    assert not p.returncode
    return out
  except:
    pass

allarches = []

compilerversionline = {}
compilers = {}
for arch in sorted(os.listdir('compilers')):
  if arch.endswith('.c'): continue
  if arch == 'default' or arch == host or arch.startswith('%s+' % host):
    allarches += [arch]
    compilers[arch] = []
    with open('compilers/%s' % arch) as f:
      for c in f.readlines():
        c = c.strip()
        cv = compilerversion(c)
        if cv == None:
          log('skipping %s compiler %s' % (arch,c))
          continue
        compilers[arch] += [c]
        cv = (c+'\n'+cv).strip().replace('\n','; ')
        compilerversionline[arch,c] = cv
        log('using %s compiler %s' % (arch,cv))

with open('build/%s/allarches' % host,'w') as f:
  for arch in allarches:
    f.write('%s\n' % arch)

firstcompiler = compilers['default'][0]

with open('build/%s/scripts/cdcompile' % host,'w') as f:
  f.write('#!/bin/sh\n')
  f.write('\n')
  f.write('cd "$1"; shift\n')
  f.write('exec %s "$@"\n' % firstcompiler)
os.chmod('build/%s/scripts/cdcompile' % host,0o755)

compilerabbrev = {}
i = 0
for arch in allarches:
  for compiler in compilers[arch]:
    compilerabbrev[arch,compiler] = 'C%d'%i
    i += 1
    log('abbreviating %s = %s %s' % (compilerabbrev[arch,compiler],arch,compiler))

if len(compilerabbrev) != len(sorted(compiler for arch in allarches for compiler in compilers[arch])):
  raise Exception('duplicate compilers not allowed; note: ok to add an arch with an empty compiler list')

os.makedirs('build/%s/compilerarch' % host,exist_ok=True)
for arch,compiler in compilerabbrev:
  with open('build/%s/compilerarch/%s' % (host,compilerabbrev[arch,compiler]),'w') as f:
    f.write('%s\n' % arch)

os.makedirs('build/%s/compilerversion' % host,exist_ok=True)
for arch,compiler in compilerabbrev:
  with open('build/%s/compilerversion/%s' % (host,compilerabbrev[arch,compiler]),'w') as f:
    f.write('%s\n' % compilerversionline[arch,compiler])

# ----- run-time checks for whether host actually supports compiler

copytree('compilers','build/%s/compilers' % host)

for arch in allarches:
  if arch == 'default': continue
  assert os.path.exists('compilers/%s.c'%arch)

  arch_csymbol = ''.join(x if x in '0123456789abcdefghijklmnopqrstuvwxyz' else '_' for x in arch)

  M = '\n'
  M += 'compilers/%s.o: compilers/%s.c\n' % (arch,arch)
  M += '\tscripts/compiledefault compilers %s c -Dsupports=%s_supports_%s\n' % (arch,projectnamespace,arch_csymbol)
  makefile = M + makefile

M = '\n'
M += 'compilers-all: %s\n' % ' '.join('compilers/%s.o'%arch for arch in allarches if arch != 'default')
M += '\ttouch compilers-all\n'
makefile = M + makefile

# ----- build-time checks for ifunc

copytree('dispatch','build/%s/dispatch' % host)
with open('build/%s/dispatch/tryifunc' % host,'w') as f:
  f.write('1\n' if tryifunc else '0\n')

M = '\n'
M += 'dispatch/ifunc: dispatch/ifunc.do dispatch/tryifunc\n'
M += '\tdispatch/ifunc.do\n'
makefile = M + makefile

# ----- crypto

operations = []
primitives = {}
sizes = {}
exports = {}
prototypes = {}

with open('api') as f:
  for line in f:
    line = line.strip()
    if line.startswith('crypto_'):
      x = line.split()
      x = x[0].split('/')
      assert len(x) == 2
      o = x[0].split('_')[1]
      if o not in operations: operations += [o]
      p = x[1]
      if o not in primitives: primitives[o] = []
      primitives[o] += [p]
      continue
    if line.startswith('#define '):
      x = line.split(' ')
      x = x[1].split('_')
      assert len(x) == 4
      assert x[0] == 'crypto'
      o = x[1]
      p = x[2]
      if (o,p) not in sizes: sizes[o,p] = ''
      sizes[o,p] += line+'\n'
      continue
    if line.endswith(');'):
      fun,args = line[:-2].split('(')
      rettype,fun = fun.split()
      fun = fun.split('_')
      o = fun[1]
      assert fun[0] == 'crypto'
      if o not in exports: exports[o] = []
      exports[o] += ['_'.join(fun[1:])]
      if o not in prototypes: prototypes[o] = []
      prototypes[o] += [(rettype,fun,args)]

with open('doc/hdoc') as f:
  hfile = f.read()

hfile += """
#ifndef %s_h
#define %s_h

#ifdef __cplusplus
extern "C" {
#endif
""" % (projectnamespace,projectnamespace)

with open('hdef') as f:
  hfile += '\n'
  hfile += f.read()

with open('version') as f:
  version = f.readlines()[0].strip()

def cstring(x):
  return '"%s"' % x.replace('\\','\\\\').replace('"','\\"').replace('\n','\\n')

hfile += '\n'
hfile += '#define %s_version %s\n' % (projectnamespace,cstring(version))
hfile += '#define %s_arch %s\n' % (projectnamespace,cstring(host))
hfile += '\n'
hfile += 'extern void %s_cpuid(unsigned int *,long long);\n' % projectnamespace

for o in operations:
  for ppos,p in enumerate(primitives[o]):
    if (o,p) not in sizes: sizes[o,p] = ''
    if len(sizes[o,p]) > 0:
      S = re.sub(' crypto_',' %s_'%projectnamespace,sizes[o,p])
      if ppos == 0:
        hfile += '\n'
        hfile += re.sub('%s_%s_%s_'%(projectnamespace,o,p),'%s_%s_'%(projectnamespace,o),S)
      hfile += '\n'
      hfile += S

    for rettype,fun,args in prototypes[o]:
      shortfun = '_'.join(fun[1:])
      pshortfun = '_'.join([o,p]+fun[2:])
      if ppos == 0:
        hfile += '\n'
        hfile += '#define %s_%s %s_%s\n' % (projectnamespace,shortfun,projectnamespace,pshortfun)
        hfile += '#define %s_dispatch_%s %s_dispatch_%s\n' % (projectnamespace,shortfun,projectnamespace,pshortfun)
      hfile += '\n'
      hfile += 'extern %s %s_%s(%s);\n' % (rettype,projectnamespace,pshortfun,args)
      hfile += 'extern %s (*%s_dispatch_%s(long long))(%s);\n' % (rettype,projectnamespace,pshortfun,args)

    if ppos == 0:
      hfile += '\n'
      hfile += '#define %s_%s_implementation %s_%s_%s_implementation\n' % (projectnamespace,o,projectnamespace,o,p)
      hfile += '#define %s_%s_compiler %s_%s_%s_compiler\n' % (projectnamespace,o,projectnamespace,o,p)
      hfile += '#define %s_dispatch_%s_implementation %s_dispatch_%s_%s_implementation\n' % (projectnamespace,o,projectnamespace,o,p)
      hfile += '#define %s_dispatch_%s_compiler %s_dispatch_%s_%s_compiler\n' % (projectnamespace,o,projectnamespace,o,p)
      hfile += '#define %s_numimpl_%s %s_numimpl_%s_%s\n' % (projectnamespace,o,projectnamespace,o,p)
    hfile += '\n'
    hfile += 'extern const char *%s_%s_%s_implementation(void);\n' % (projectnamespace,o,p)
    hfile += 'extern const char *%s_%s_%s_compiler(void);\n' % (projectnamespace,o,p)
    hfile += 'extern const char *%s_dispatch_%s_%s_implementation(long long);\n' % (projectnamespace,o,p)
    hfile += 'extern const char *%s_dispatch_%s_%s_compiler(long long);\n' % (projectnamespace,o,p)
    hfile += 'extern long long %s_numimpl_%s_%s(void);\n' % (projectnamespace,o,p)

hfile += """
#ifdef __cplusplus
}
#endif

#endif
"""

with open('build/%s/package/include/%s.h' % (host,projectnamespace),'w') as f:
  f.write(hfile)
os.chmod('build/%s/package/include/%s.h' % (host,projectnamespace),0o644)

for o in operations:
  for p in primitives[o]:
    cryptoh = ''
    cryptoh += '#ifndef crypto_%s_%s_h\n' % (o,p)
    cryptoh += '#define crypto_%s_%s_h\n' % (o,p)
    cryptoh += '\n'
    for rettype,fun,args in prototypes[o]:
      pshortfun = '_'.join([o,p]+fun[2:])
      cryptoh += '#define crypto_%s %s_%s\n' % (pshortfun,projectnamespace,pshortfun)
    cryptoh += '\n'
    cryptoh += sizes[o,p]
    cryptoh += '\n'
    for rettype,fun,args in prototypes[o]:
      pshortfun = '_'.join([o,p]+fun[2:])
      cryptoh += 'extern %s crypto_%s(%s);\n' % (rettype,pshortfun,args)
    cryptoh += '\n'
    cryptoh += '#endif\n'
  
    with open('build/%s/include-build/crypto_%s_%s.h' % (host,o,p),'w') as f:
      f.write(cryptoh)

doth = {}
for o in operations:
  for p in primitives[o]:
    result = ''
    result += '#ifndef crypto_%s_h\n' % o
    result += '#define crypto_%s_h\n' % o
    result += '\n'
    for m in exports[o]:
      result += '#define crypto_%s CRYPTO_NAMESPACE(%s)\n' % (m,m)
    result += '\n'
    result += sizes[o,p]
    result += re.sub('crypto_%s_%s_'%(o,p),'crypto_%s_'%o,sizes[o,p])
    result += '\n'
    for rettype,fun,args in prototypes[o]:
      result += 'extern %s %s(%s);\n' % (rettype,'_'.join(fun),args)
    result += '\n'
    result += '#endif\n'
    doth[o,p] = result

impls = {}
for o in operations:
  for p in primitives[o]:
    impls[o,p] = []
    for i in sorted(os.listdir('crypto_%s/%s' % (o,p))):
      impldir = 'crypto_%s/%s/%s' % (o,p,i)
      if not os.path.isdir(impldir): continue
      if os.stat(impldir).st_mode & 0o1000 == 0o1000:
        log('skipping sticky %s' % impldir)
        continue
  
      implarch = None
      if os.path.exists('%s/architectures' % impldir):
        with open('%s/architectures' % impldir) as f:
          for line in f:
            line = line.strip().split()
            if len(line) == 0: continue
            if line[0] != host: continue
            implarch = line
      else:
        implarch = [host]
  
      if implarch == None: continue
      assert implarch[0] == host

      impls[o,p] += [(i,impldir,implarch)]

undisciplined = set()
file_hash = {}
inclusions = {}
namespace_defines = {}
namespace_uses = {}
namespace_definedin = {}

def file_process(dir,fn,substitutes={}):
  if (dir,fn) in file_hash: return

  if fn in substitutes:
    x = substitutes[fn]
  else:
    with open(dir+'/'+fn) as f:
      x = f.read()

  file_hash[dir,fn] = sha512(x.encode('utf8'))
  inclusions[dir,fn] = []
  namespace_defines[dir,fn] = []
  namespace_uses[dir,fn] = []

  for line in x.splitlines():
    line = line.split()

    if line[:3] == ['//','linker','define']:
      for x in line[3:]:
        namespace_defines[dir,fn] += [x]
        if (dir,x) in namespace_definedin:
          undisciplined.add(dir)
        namespace_definedin[dir,x] = fn

    if line[:3] == ['//','linker','use']:
      for x in line[3:]:
        namespace_uses[dir,fn] += [x]

    if line[:1] != ['#include']: continue
    if len(line) < 2: continue
    subfn = line[1]
    if not subfn.startswith('"'): continue
    if not subfn.endswith('"'): continue
    subfn = subfn[1:-1]
    if subfn == 'randombytes.h': continue
    if subfn.startswith('crypto_') and subfn not in substitutes: continue
    inclusions[dir,fn] += [subfn]
    file_process(dir,subfn,substitutes)

checksum_cache = {}
checksum_inprogress = set()
def checksum(dir,fn):
  if dir in undisciplined:
    return os.urandom(32)
  if (dir,fn) not in checksum_cache:
    if (dir,fn) in checksum_inprogress:
      undisciplined.add(dir)
      return os.urandom(32)
    checksum_inprogress.add((dir,fn))
    result = file_hash[dir,fn]
    for subfn in inclusions[dir,fn]:
      result += checksum(dir,subfn)
    for symbol in namespace_uses[dir,fn]:
      if (dir,symbol) in namespace_definedin:
        result += checksum(dir,namespace_definedin[dir,symbol])
    result = sha512(result)
    checksum_cache[dir,fn] = result
    checksum_inprogress.remove((dir,fn))
  return checksum_cache[dir,fn]

checksum_context = {}

for o in operations:
  for p in primitives[o]:
    for i,impldir,implarch in impls[o,p]:
      for fn in sorted(os.listdir(impldir)):
        if fn.endswith('.c') or fn.endswith('.S'):
          file_process(impldir,fn,substitutes={'crypto_%s.h'%o:doth[o,p]})
      for fn in sorted(os.listdir(impldir)):
        if fn.endswith('.c') or fn.endswith('.S'):
          c = checksum(impldir,fn)
          if c not in checksum_context:
            checksum_context[c] = []
          checksum_context[c] += [(o,p,i,impldir,implarch,fn)]
        
copywithinclusions_done = set()

def copywithinclusions(targetdir,dir,fn,substitutes={}):
  if (targetdir,fn) in copywithinclusions_done: return
  copywithinclusions_done.add((targetdir,fn))

  if fn in substitutes:
    with open(targetdir+'/'+fn,'w') as f:
      f.write(substitutes[fn])
    shutil.copystat(dir,targetdir+'/'+fn)
  else:
    dirlinksym(targetdir,fn,'../'*(len(targetdir.split('/'))-2)+'src/'+dir+'/'+fn)
  for subfn in inclusions[dir,fn]:
    copywithinclusions(targetdir,dir,subfn,substitutes)

unified_files = {}
# crypto_o/p/i/fn will be unified across multiple (o,p,i,fn)
#   if fn in unified_files[o,p,i]

for o in operations:
  for p in primitives[o]:
    for i,impldir,implarch in impls[o,p]:
      unified_files[o,p,i] = set()
      unified_symbols = set()
      for fn in sorted(os.listdir(impldir)):
        if not(fn.endswith('.S') or fn.endswith('.c')): continue
        if len(namespace_defines[impldir,fn]) == 0: continue
        c = checksum(impldir,fn)
        if c not in checksum_context: continue
        if len(checksum_context[c]) == 1: continue
        unified_files[o,p,i].add(fn)
        for symbol in namespace_defines[impldir,fn]:
          unified_symbols.add(symbol)

      progress = True
      while progress:
        progress = False
        for fn in sorted(unified_files[o,p,i]):
          if any(symbol not in unified_symbols for symbol in namespace_uses[impldir,fn]):
            progress = True
            unified_files[o,p,i].remove(fn)
            for symbol in namespace_defines[impldir,fn]:
              unified_symbols.remove(symbol)

unified_built = set()
checksum_unified = {}

unified_counter = {}

def unified_dir(o,p,i,fn):
  allowedchars = '0123456789'
  allowedchars += 'abcdefghijklmnopqrstuvwxyz'
  allowedchars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  base = ''.join(c for c in fn if c in allowedchars)
  if len(base) == 0: base = 'x'

  if base not in unified_counter:
    unified_counter[base] = 0
  unified_counter[base] += 1
  return 'unified/%s/%d' % (base,unified_counter[base])

for o in operations:
  for p in primitives[o]:
    for i,impldir,implarch in impls[o,p]:
      if impldir in undisciplined:
        log('warning: undisciplined %s' % impldir)
        unified_files[o,p,i] = []
  
      for fn in unified_files[o,p,i]:
        if acceptfn_shared(fn):
          c = checksum(impldir,fn)
          if c not in checksum_unified:
            checksum_unified[c] = unified_dir(o,p,i,fn)
        else:
          for arch in compilers:
            if any(implarchreq not in arch.split('+')[1:] for implarchreq in implarch[1:]):
              continue
            for compiler in compilers[arch]:
              c = checksum(impldir,fn)
              if (c,compiler) not in checksum_unified:
                checksum_unified[c,compiler] = unified_dir(o,p,i,fn)

ofiles = []
opicdirs = []
builddirs = []

opicdir2dependencies = {}

def impl2symbol(i):
  return i.replace('-','').replace('_','')

for o in operations:
  for p in primitives[o]:
    for i,impldir,implarch in impls[o,p]:
      builtshared = False
      needshared = False
      shareddeps = []

      for arch in compilers:
        if any(implarchreq not in arch.split('+')[1:] for implarchreq in implarch[1:]):
          continue
  
        for compiler in compilers[arch]:
          if not builtshared:
            shareddeps += ['%s/%s/%s/shared' % (o,p,i)]
    
            builddirs += ['%s/%s/%s/shared' % (o,p,i)]
            shared_builddir = 'build/%s/%s/%s/%s/shared' % (host,o,p,i)
            os.makedirs(shared_builddir,exist_ok=True)
            copytree(impldir,shared_builddir,acceptfn_shared)
    
            nonunified_shared_compiler = compiler
            for fn2 in sorted(unified_files[o,p,i]):
              if not acceptfn_shared(fn2): continue
              for symbol in namespace_defines[impldir,fn2]:
                c2 = checksum(impldir,fn2)
                nonunified_shared_compiler += ' -D%s_%s_%s_%s_%s_%s=%s_%s_%s_%s' % (
                  projectnamespace,o,p,impl2symbol(i),'shared',symbol,
                  projectnamespace,checksum_unified[c2].replace('/','_'),'shared',symbol
                )
            with open('%s/compiler' % shared_builddir,'w') as f:
              f.write(nonunified_shared_compiler+'\n')

            for fn in sorted(os.listdir(impldir)):
              if not acceptfn_shared(fn): continue
              if fn.endswith('.S') or fn.endswith('.c'):
                needshared = True

                base = fn[:-2]
                ext = fn[-1:]

                if fn in unified_files[o,p,i]:
                  if not acceptfn_shared(fn): continue
                  c = checksum(impldir,fn)
                  shareddeps += ['%s/shared' % checksum_unified[c]]
                  if checksum_unified[c] not in unified_built:
                    builddirs += ['%s/shared' % checksum_unified[c]]
                    unifieddir = 'build/%s/%s/shared' % (host,checksum_unified[c])
                    os.makedirs(unifieddir,exist_ok=True)
                    copywithinclusions(unifieddir,impldir,fn,substitutes={'crypto_%s.h'%o:doth[o,p]})
                    unified_shared_compiler = compiler
                    for fn2 in sorted(unified_files[o,p,i]):
                      if not acceptfn_shared(fn2): continue
                      for symbol in namespace_defines[impldir,fn2]:
                        c2 = checksum(impldir,fn2)
                        unified_shared_compiler += ' -D%s_%s_%s_%s=%s_%s_%s_%s' % (
                          projectnamespace,checksum_unified[c].replace('/','_'),'shared',symbol,
                          projectnamespace,checksum_unified[c2].replace('/','_'),'shared',symbol
                        )
                    with open('%s/compiler' % unifieddir,'w') as f:
                      f.write(unified_shared_compiler+'\n')
                    makefile += '\n'
                    ofiles += ['%s/shared/%s.o' % (checksum_unified[c],base)]
                    makefile += '%s/shared/%s.o: %s/shared/%s\n' % (checksum_unified[c],base,checksum_unified[c],fn)
                    makefile += '\tscripts/compile %s/shared %s %s %s\n' % (checksum_unified[c],base,ext,projectnamespace)
                    unified_built.add(checksum_unified[c])
                else:
                  makefile += '\n'
                  ofiles += ['%s/%s/%s/shared/%s.o' % (o,p,i,base)]
                  makefile += '%s/%s/%s/shared/%s.o: %s/%s/%s/shared/%s\n' % (o,p,i,base,o,p,i,fn)
                  makefile += '\tscripts/compile %s/%s/%s/shared %s %s %s\n' % (o,p,i,base,ext,projectnamespace)

            builtshared = True

          dependencies = list(shareddeps) if needshared else []

          compilerdir = compilerabbrev[arch,compiler]
  
          opicdir = '%s/%s/%s/%s' % (o,p,i,compilerdir)
          opicdirs += [opicdir]
          builddirs += [opicdir]
          builddir = 'build/%s/%s/%s/%s/%s' % (host,o,p,i,compilerdir)
          os.makedirs(builddir,exist_ok=True)
          copytree(impldir,builddir,acceptfn_nonshared)
  
          nonunified_nonshared_compiler = compiler
          for fn in sorted(unified_files[o,p,i]):
            for symbol in namespace_defines[impldir,fn]:
              c = checksum(impldir,fn)
              if acceptfn_shared(fn):
                nonunified_nonshared_compiler += ' -D%s_%s_%s_%s_%s_%s=%s_%s_%s_%s' % (
                  projectnamespace,o,p,impl2symbol(i),'shared',symbol,
                  projectnamespace,checksum_unified[c].replace('/','_'),'shared',symbol
                )
              else:
                nonunified_nonshared_compiler += ' -D%s_%s_%s_%s_%s_%s=%s_%s_%s_%s' % (
                  projectnamespace,o,p,impl2symbol(i),compilerdir,symbol,
                  projectnamespace,checksum_unified[c,compiler].replace('/','_'),compilerdir,symbol
                )

          with open('%s/crypto_%s.h' % (builddir,o),'w') as f:
            f.write(doth[o,p])
          with open('%s/compiler' % builddir,'w') as f:
            f.write(nonunified_nonshared_compiler+'\n')
  
          for fn in sorted(os.listdir(impldir)):
            if not acceptfn_nonshared(fn): continue
            if fn.endswith('.c'):
              base = fn[:-2]
              ext = fn[-1:]

              if fn in unified_files[o,p,i]:
                if not acceptfn_nonshared(fn): continue
                c = checksum(impldir,fn)
                dependencies += ['%s/%s' % (checksum_unified[c,compiler],compilerdir)]
                if checksum_unified[c,compiler] not in unified_built:
                  unifieddir = 'build/%s/%s/%s' % (host,checksum_unified[c,compiler],compilerdir)
                  builddirs += ['%s/%s' % (checksum_unified[c,compiler],compilerdir)]
                  os.makedirs(unifieddir,exist_ok=True)
                  copywithinclusions(unifieddir,impldir,fn,substitutes={'crypto_%s.h'%o:doth[o,p]})
                  unified_nonshared_compiler = compiler
                  for fn2 in sorted(unified_files[o,p,i]):
                    for symbol in namespace_defines[impldir,fn2]:
                      c2 = checksum(impldir,fn2)
                      if acceptfn_shared(fn2):
                        unified_nonshared_compiler += ' -D%s_%s_%s_%s=%s_%s_%s_%s' % (
                          projectnamespace,checksum_unified[c,compiler].replace('/','_'),'shared',symbol,
                          projectnamespace,checksum_unified[c2].replace('/','_'),'shared',symbol
                        )
                      else:
                        unified_nonshared_compiler += ' -D%s_%s_%s_%s=%s_%s_%s_%s' % (
                          projectnamespace,checksum_unified[c,compiler].replace('/','_'),compilerdir,symbol,
                          projectnamespace,checksum_unified[c2,compiler].replace('/','_'),compilerdir,symbol
                        )
                  with open('%s/compiler' % unifieddir,'w') as f:
                    f.write(unified_nonshared_compiler+'\n')
                  makefile += '\n'
                  ofiles += ['%s/%s/%s.o' % (checksum_unified[c,compiler],compilerdir,base)]
                  makefile += '%s/%s/%s.o: %s/%s/%s\n' % (checksum_unified[c,compiler],compilerdir,base,checksum_unified[c,compiler],compilerdir,fn)
                  makefile += '\tscripts/compile %s/%s %s %s %s\n' % (checksum_unified[c,compiler],compilerdir,base,ext,projectnamespace)
                  unified_built.add(checksum_unified[c,compiler])
              else:
                makefile += '\n'
                ofiles += ['%s/%s/%s/%s/%s.o' % (o,p,i,compilerdir,base)]
                makefile += '%s/%s/%s/%s/%s.o: %s/%s/%s/%s/%s\n' % (o,p,i,compilerdir,base,o,p,i,compilerdir,fn)
                makefile += '\tscripts/compile %s/%s/%s/%s %s %s %s\n' % (o,p,i,compilerdir,base,ext,projectnamespace)

          opicdir2dependencies[opicdir] = dependencies
          if len(dependencies) > 0:
            with open(builddir+'/dependencies','w') as f:
              f.write('\n'.join(dependencies))

# ----- compilerdirs

compilerdirs = {}
for o in operations:
  for p in primitives[o]:
    for i,impldir,implarch in impls[o,p]:
      for fn in sorted(os.listdir(impldir)):
        if not acceptfn_shared(fn): continue
        if not (fn.endswith('.S') or fn.endswith('.c')): continue
        if fn in unified_files[o,p,i]:
          c = checksum(impldir,fn)
          builddir = checksum_unified[c]+'/shared'
        else:
          builddir = f'{o}/{p}/{i}/shared'
        for arch in compilers:
          if any(implarchreq not in arch.split('+')[1:] for implarchreq in implarch[1:]):
            continue
          for compiler in compilers[arch]:
            compilerdir = compilerabbrev[arch,compiler]
            if builddir not in compilerdirs:
              compilerdirs[builddir] = []
            compilerdirs[builddir] += [compilerdir]

for builddir in compilerdirs:
  with open(f'build/{host}/{builddir}/compilerdirs','w') as f:
    for sharedcompilerdir in sorted(set(compilerdirs[builddir])):
      f.write(sharedcompilerdir+'\n')

# ----- dispatch

with open('build/%s/opicdirs'%host,'w') as f:
  for opicdir in opicdirs:
    f.write(opicdir+'\n')

with open('build/%s/builddirs'%host,'w') as f:
  for builddir in builddirs:
    f.write(builddir+'\n')

M = ''

M += 'compiledimplementations: opicdirs scripts/compiledimplementations \\\n'
for opicdir in opicdirs:
  M += '%s/allcompiled \\\n' % opicdir
M += '\n'
M += '\tscripts/compiledimplementations < opicdirs > compiledimplementations\n'
M += '\n'

M += 'result-impl: \\\n'
for builddir in builddirs:
  M += '%s/result \\\n' % builddir
M += '\n'
M += '\tscripts/result-impl < builddirs > result-impl\n'
M += '\n'

for opicdir in opicdirs:
  if len(opicdir2dependencies[opicdir]) > 0:
    dependencies_compiled = ' '.join(builddir+'/allcompiled' for builddir in opicdir2dependencies[opicdir])
    M += '%s/allcompiled: %s\n' % (opicdir,dependencies_compiled)
    M += '\n'

builddir_ofiles = {}

for builddir in builddirs:
  assert len(builddir.split('/')) == 4
  builddir_ofiles[builddir] = []

for ofile in ofiles:
  builddir = ofile.split('/')[:4]
  if len(builddir) < 4: continue
  builddir = '/'.join(builddir)
  builddir_ofiles[builddir] += [ofile]

for builddir in builddirs:
  M += '%s/allcompiled: %s\n' % (builddir,' '.join(builddir_ofiles[builddir]))
  M += '\ttouch %s/allcompiled\n' % builddir
  M += '\n'

for builddir in builddirs:
  deps = [builddir+'/result-namespace',builddir+'/result-insns']
  deps += builddir_ofiles[builddir]
  deps = ' '.join(deps)
  results = [builddir+'/result-namespace',builddir+'/result-insns']
  for ofile in builddir_ofiles[builddir]:
    ofile = '/'.join(ofile.split('/')[4:])
    results += [builddir+'/result-compile-'+ofile[:-2]]
  results = ' '.join(results)
  M += '%s/result: %s\n' % (builddir,deps)
  M += '\tcat %s > %s/result\n' % (results,builddir)
  M += '\n'

for builddir in builddirs:
  M += '%s/result-insns: %s/allcompiled scripts/checkinsns\n' % (builddir,builddir)
  M += '\tscripts/checkinsns %s %s\n' % (host,builddir)
  M += '\n'

for builddir in builddirs:
  M += '%s/result-namespace: %s/allcompiled scripts/checknamespace\n' % (builddir,builddir)
  M += '\tscripts/checknamespace %s %s\n' % (projectnamespace,builddir)
  M += '\n'

makefile = M + makefile

selectedlist = ' '.join('selected/%s_%s' % (o,p) for o in operations for p in primitives[o])
M = 'usedimplementations: scripts/usedimplementations %s\n' % selectedlist
M += '\tscripts/usedimplementations %s > usedimplementations\n' % selectedlist
M += '\n'
makefile = M + makefile

os.makedirs('build/%s/selected' % host,exist_ok=True)
M = ''
for o in operations:
  for p in primitives[o]:
    M += 'selected/%s_%s: scripts/selected compiledimplementations\n' % (o,p)
    M += '\tscripts/selected %s %s %s %s < compiledimplementations > selected/%s_%s\n' % (o,p,host,trim,o,p)
    M += '\n'
makefile = M + makefile

for goal in 'auto','manual':
  os.makedirs('build/%s/dispatch-%s' % (host,goal),exist_ok=True)

  M = ''
  for o in operations:
    for p in primitives[o]:
      M += 'dispatch-%s/%s_%s.c: scripts/dispatch selected/%s_%s dispatch/ifunc\n' % (goal,o,p,o,p)
      M += '\tscripts/dispatch %s %s %s %s %s < selected/%s_%s > dispatch-%s/%s_%s.c\n' % (goal,o,p,host,projectnamespace,o,p,goal,o,p)
      M += '\n'
  makefile = M + makefile

  M = ''
  for o in operations:
    for p in primitives[o]:
      M += 'dispatch-%s/%s_%s.o: dispatch-%s/%s_%s.c\n' % (goal,o,p,goal,o,p)
      M += '\tscripts/compiledefault dispatch-%s %s_%s c\n' % (goal,o,p)
      M += '\n'
  makefile = M + makefile

  M = 'dispatch-%s-all: \\\n' % goal
  for o in operations:
    for p in primitives[o]:
      M += 'dispatch-%s/%s_%s.o \\\n' % (goal,o,p)
  M += '\n'
  M += '\ttouch dispatch-%s-all\n' % goal
  M += '\n'
  makefile = M + makefile

# ----- projectlibrary

M = 'odirs: usedimplementations\n'
M += '\t( cat usedimplementations; echo dispatch-auto; echo dispatch-manual; echo compilers; echo cpuid; echo cryptoint ) > odirs\n'
M += '\n'
makefile = M + makefile

M = 'ofiles: scripts/ofiles odirs usedimplementations dispatch-auto-all dispatch-manual-all compilers-all cpuid-all cryptoint-all\n'
M += '\tscripts/ofiles < odirs\n'
M += '\n'
makefile = M + makefile

M = 'package/lib/lib%s.a: scripts/staticlib ofiles\n' % projectlibrary
M += '\tscripts/staticlib lib%s\n' % projectlibrary
M += '\n'
makefile = M + makefile

M = f'package/lib/lib{projectlibrary}.{so1}: scripts/sharedlib ofiles\n'
M += '\tscripts/sharedlib lib%s -lrandombytes\n' % projectlibrary
M += '\n'
makefile = M + makefile

M = f'package/lib/lib{projectlibrary}.{so}: package/lib/lib{projectlibrary}.{so1}\n'
M += f'\trm -f package/lib/lib{projectlibrary}.{so}\n'
M += f'\tln -s lib{projectlibrary}.{so1} package/lib/lib{projectlibrary}.{so}\n'
M += '\n'
makefile = M + makefile

# ----- cpuid

cpuid = host
if not os.path.exists('cpuid/%s.c' % host):
  cpuid = 'default'

M = '\n'
M += 'cpuid-all: cpuid/%s.o\n' % cpuid
M += '\ttouch cpuid-all\n'
M += '\n'
M += 'cpuid/%s.o: cpuid/%s.c\n' % (cpuid,cpuid)
M += '\tscripts/compiledefault cpuid %s c\n' % cpuid
makefile += M

# ----- command

copytree('command','build/%s/command'%host)
dirlinksym('build/%s/command'%host,'bin','../package/bin')
dirlinksym('build/%s/command'%host,'lib','../package/lib')
dirlinksym('build/%s/command'%host,'include-build','../include-build')
dirlinksym('build/%s/command'%host,'include','../package/include')

with open('build/%s/command/link' % host,'w') as f:
  f.write('#!/bin/sh\n')
  f.write('target="$1"; shift\n')
  f.write('%s \\\n' % firstcompiler)
  f.write('  -fvisibility=hidden \\\n')
  f.write('  -o "$target" "$@"\n')
os.chmod('build/%s/command/link' % host,0o755)

commands = []

for fn in sorted(os.listdir('command')):
  if not fn.endswith('.c'): continue

  base = fn[:-2]
  deps = 'command/%s.o'%base
  link = 'cd command && ./link bin/%s %s.o'%(base,base)
  syslink = ''

  with open('command/%s' % fn) as f:
    for line in f:
      line = line.strip().split()
      if len(line) < 1: continue
      if line[0] != '#include': continue
      if '-lcpucycles' in line:
        syslink += ' -lcpucycles'
      if '-lrandombytes' in line:
        syslink += ' -lrandombytes'
      if '-l%s'%projectlibrary in line:
        deps += f' package/lib/lib{projectlibrary}.{so}'
        link += f' lib/lib{projectlibrary}.{so}'

  link += syslink

  M = 'command/%s.o: command/%s.c\n' % (base,base)
  M += '\tscripts/compiledefault command %s c -I include -I include-build\n' % base
  M += '\n'
  makefile = M + makefile
  M = 'package/bin/%s: %s\n' % (base,deps)
  M += '\t%s\n' % link
  M += '\n'
  makefile = M + makefile
  commands += ['package/bin/%s' % base]

M = f'package/bin/{projectlibrary}-fulltest: scripts/fulltest-convert scripts/fulltest-top usedimplementations\n'
M += f'\tscripts/fulltest-convert {projectlibrary} < usedimplementations > package/bin/{projectlibrary}-fulltest\n'
M += '\n'
makefile = M + makefile
commands += [f'package/bin/{projectlibrary}-fulltest']

M = 'commands: %s\n' % ' '.join(commands)
M += '\n'
makefile = M + makefile

# ----- manual pages

for manpage in sorted(os.listdir('doc/man')):
  section = 'man%s' % manpage[-1]
  targetdir = 'build/%s/package/man/%s' % (host,section)
  os.makedirs(targetdir,exist_ok=True)
  shutil.copy2('doc/man/%s'%manpage,'%s/%s'%(targetdir,manpage))

# ----- make install

M = 'install: scripts/install default\n'
M += '\tscripts/install %s\n' % prefix
M += '\n'
makefile = M + makefile

# ----- make default

M = f'default: package/lib/lib{projectlibrary}.a package/lib/lib{projectlibrary}.{so} package/lib/lib{projectlibrary}.{so1} \\\n'
M += 'commands result-impl\n'
M += '\tcat result-impl\n'
M += '\n'
makefile = M + makefile

with open('build/%s/Makefile' % host,'w') as f:
  f.write(makefile)

# ----- build/0, build/Makefile

dirlinksym('build','0',host)

with open('build/Makefile','w') as f:
  f.write('default:\n')
  f.write('\tcd %s && $(MAKE)\n' % host)
  f.write('\n')
  f.write('install:\n')
  f.write('\tcd %s && $(MAKE) install\n' % host)
  f.write('\n')
  f.write('clean:\n')
  f.write('\trm -r %s\n' % host)

log('configure done')
with open('build/%s/configure.log'%host,'a') as f:
  f.write(configurelog)
with open('build/%s/configure.log.latest'%host,'w') as f:
  f.write(configurelog)
