#!/usr/pkg/bin/python3.13

import string
import re
import platform
import sys
import os
import signal
import subprocess
import shutil
import datetime

def readfile(fn):
  with open(fn,'r') as f:
    return f.read()

def writefilebinary(fn,s):
  with open(fn,'wb') as f:
    f.write(s)

def writefile(fn,s):
  with open(fn,'w') as f:
    f.write(s)

def copymkdir(old,dir):
  try:
    os.makedirs(dir)
  except:
    pass
  shutil.copy(old,dir)

project = 'djbsort'

version = readfile('version').strip()

shorthostname = platform.node().split('.')[0].lower()
okcharacters = string.ascii_letters + string.digits
shorthostname = ''.join(c for c in shorthostname if c in okcharacters)
shorthostname = 'pkgsrchostname'

startdir = os.getcwd()

compiled = '%s/link-build/obj-%s/%s' % (startdir,version,shorthostname)

work = '%s/link-build/test-%s/%s' % (startdir,version,shorthostname)
shutil.rmtree(work,True)
os.makedirs(work)

notes = '%s/notes' % work
os.makedirs(notes)
log = open('%s/log' % notes,'w')

tmp = '%s/tmp' % work

objproject = '%s/obj' % work
shutil.rmtree(objproject,True)
os.makedirs(objproject)

testlib = '%s/lib' % work
shutil.rmtree(testlib,True)
os.makedirs(testlib)

installlib = '%s/link-install/run-%s/%s/lib' % (startdir,version,shorthostname)
shutil.rmtree(installlib,True)
os.makedirs(installlib)
rpath = os.path.realpath(installlib)

command = '%s/link-install/run-%s/%s/command' % (startdir,version,shorthostname)
shutil.rmtree(command,True)
os.makedirs(command)


logprevious = None

def lognow(x,y=''):
  global logprevious

  x = re.sub('\n','_',x)
  output = '%s\n' % x
  if y:
    try:
      y = y.decode()
    except:
      pass
    for z in y.splitlines():
      output += '> %s\n' % z

  now = datetime.datetime.now()
  if logprevious == None: logprevious = now
  duration = (now - logprevious).total_seconds()
  logprevious = now

  log.write('%s === %9f === %s' % (now.ctime(),duration,output))
  log.flush()
  sys.stdout.write(output)
  sys.stdout.flush()

lognow('test starting')
lognow('version %s' % version)
lognow('hostname %s' % shorthostname)

signals = dict((getattr(signal,x),x) for x in dir(signal) if x.startswith('SIG') and '_' not in x)

def returncodestr(n):
  if -n in signals: return signals[-n]
  return str(n)

def addlibproject(abi,fo,membername):
  staticlib = '%s/%s/lib%s.a' % (installlib,abi,project)
  doto = '%s/%s/%s' % (objproject,abi,membername)

  if os.path.exists(doto):
    lognow('error: overwriting %s' % membername)
    return
  shutil.copy(fo,doto)

  try:
    cmd = 'ar cr %s %s' % (staticlib,doto)
    p = subprocess.Popen(cmd.split(),stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
    out,err = p.communicate()
    if out:
      lognow('archiver stdout',out)
    if err:
      lognow('archiver stderr',err)
    if p.returncode:
      lognow('archiver failed exit %s' % returncodestr(p.returncode))
  except Exception as e:
    lognow('archiver failed %s' % e)
  try:
    cmd = 'ranlib %s' % staticlib
    p = subprocess.Popen(cmd.split(),stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
    out,err = p.communicate()
    if out:
      lognow('ranlib stdout',out)
    if err:
      lognow('ranlib stderr',err)
    if p.returncode:
      lognow('ranlib failed exit %s' % returncodestr(p.returncode))
  except Exception as e:
    lognow('ranlib failed %s' % e)

def link(c,c_,tmp,dir,exe,o):
  try:
    cmd = '%s -fvisibility=hidden -o %s %s' % (c,exe,' '.join(o))
    p = subprocess.Popen(cmd.split(),cwd=tmp,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
    out,err = p.communicate()
    assert not err
    if out:
      lognow('linker output',out)
      try:
        os.makedirs('%s/%s/%s' % (notes,c_,dir))
      except:
        pass
      writefilebinary('%s/%s/%s/%s' % (notes,c_,dir,exe),out)
    if p.returncode:
      lognow('%s/%s linker failed %s' % (dir,exe,returncodestr(p.returncode)))
      return False
    return True
  except Exception as e:
    lognow('%s/%s linker exited %s' % (dir,exe,e))
    return False

def run(c,c_,tmp,dir,exe):
  try:
    cmd = './%s' % exe
    p = subprocess.Popen(cmd.split(),cwd=tmp,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
    out,err = p.communicate()
    if err:
      lognow('test stderr',err)
      try:
        os.makedirs('%s/%s/%s' % (notes,c_,dir))
      except:
        pass
      writefilebinary('%s/%s/%s/%s' % (notes,c_,dir,exe),err)
    if p.returncode:
      lognow('%s/%s test exited %s' % (dir,exe,returncodestr(p.returncode)))
      return
    return out.decode()
  except Exception as e:
    lognow('%s/%s test failed %s' % (dir,exe,e))
    return

# ----- abi and syslibs

compilers = {}
compilers['c'] = readfile('compilers/c').splitlines()

compilerabi = {}
abis = []
syslibs = {}

for c in compilers['c']:
  c = c.strip()
  if c == '': continue
  c_ = re.sub(' ','_',c)
  compilerabi[c] = c_
  syslibs[c] = []

  shutil.rmtree(tmp,True)
  dir = '%s/%s/compilers' % (compiled,c_)
  if not os.path.exists(dir): continue

  shutil.copytree(dir,tmp)
  if link(c,c_,tmp,'compilers','abiname',['abiname.o']):
    abi = run(c,c_,tmp,'compilers','abiname')
    if abi:
      abi = abi.strip()
      compilerabi[c] = abi
      try:
        os.makedirs('%s/%s' % (objproject,abi))
        os.makedirs('%s/%s' % (testlib,abi))
        os.makedirs('%s/%s' % (installlib,abi))
        addlibproject(abi,'%s/base.o' % tmp,'base.o')
        os.symlink('../../include','%s/%s/include' % (installlib,abi))
        os.symlink(abi,'%s/0' % installlib)
      except:
        pass

  lognow('abi %s %s %s' % (compilerabi[c],c,' '.join(syslibs[c])))
  if not compilerabi[c] in abis:
    abis += [compilerabi[c]]

# XXX: check compatibility of compilers with same alleged abi

# ----- cpucycles

abicounter = {}

for counter in sorted(os.listdir('cpucycles')):
  if not os.path.isdir('cpucycles/%s' % counter): continue

  for c in compilers['c']:
    c = c.strip()
    if c == '': continue
    c_ = re.sub(' ','_',c)

    dir = '%s/%s/cpucycles/%s' % (compiled,c_,counter)
    if not os.path.exists(dir): continue

    if os.path.exists('%s/architectures' % dir):
      if all(dirabi.strip() != compilerabi[c]
             for dirabi in readfile('%s/architectures' % dir).splitlines()):
        lognow('cpucycles/%s skipping architecture %s' % (counter,c))
        continue

    abi = compilerabi[c]
    lognow('cpucycles/%s testing %s' % (counter,c))
    shutil.rmtree(tmp,True)
    shutil.copytree(dir,tmp)
    if link(c,c_,tmp,'cpucycles/%s' % counter,'test',['test.o','cpucycles.o','implementation.o']):
      cycles = run(c,c_,tmp,'cpucycles/%s' % counter,'test')
      if cycles:
        cycles = int(cycles.strip())
        lognow('cpucycles/%s cycles %s %s' % (counter,cycles,c))
        if abi not in abicounter or cycles < abicounter[abi][0]:
          abicounter[abi] = (cycles,counter,c)

for abi in abis:
  if abi not in abicounter:
    raise Exception('no working cycle counter for %s' % abi)

  cycles,counter,c = abicounter[abi]
  lognow('selected %s cpucycles/%s %s %s' % (abi,counter,cycles,c))
  c_ = re.sub(' ','_',c)

  dir = '%s/%s/cpucycles/%s' % (compiled,c_,counter)
  addlibproject(abi,'%s/cpucycles.o' % dir,'cpucycles.o')
  addlibproject(abi,'%s/implementation.o' % dir,'cpucycles_implementation.o')

# ----- sorting

types = readfile('TYPES').splitlines()

for t in types:
  t = t.strip()
  if t == '': continue
  if not os.path.isdir(t): continue
  o = '%s_sort' % t

  cyclesimpl = {}
    
  for impl in sorted(os.listdir(t)):
    implementationdir = '%s/%s' % (t,impl)
    if not os.path.isdir(implementationdir): continue
    opi = 'djbsort_%s_impl' % t

    files = sorted(os.listdir(implementationdir))
    cfiles = [x for x in files if x.endswith('.c')]
    sfiles = [x for x in files if x.endswith('.s') or x.endswith('.S')]
    files = cfiles + sfiles
    if 'compiler.c' not in files: files += ['compiler.c']
    if 'version.c' not in files: files += ['version.c']
    if 'implementation.c' not in files: files += ['implementation.c']

    files = ['%s.o' % x[:-2] for x in files]

    ok = True
    for f in files:
      if f[0] == '-':
        lognow('%s skipping because of invalid filename %s' % (implementationdir,f))
        ok = False
      for c in f:
        if c not in string.ascii_letters + string.digits + '._-':
          lognow('%s skipping because of invalid filename %s' % (implementationdir,f))
          ok = False

    if not ok: continue

    for c in compilers['c']:
      c = c.strip()
      if c == '': continue
      c_ = re.sub(' ','_',c)
      abi = compilerabi[c]

      dir = '%s/%s/%s' % (compiled,c_,implementationdir)
      if not os.path.isdir(dir): continue

      if os.path.exists('%s/architectures' % dir):
        if all(dirabi.strip() != abi
               for dirabi in readfile('%s/architectures' % dir).splitlines()):
          lognow('%s skipping architecture %s' % (implementationdir,c))
          continue

      shutil.rmtree(tmp,True)
      shutil.copytree(dir,tmp)

      copt = c
      copt += ' -L%s/%s' % (installlib,abi)
      libs = []
      libs += ['-l%s' % project]
      libs += syslibs[c]

      trylibs = libs

      lognow('%s/cycles testing %s' % (implementationdir,c))

      # see whether files are PIC-compatible
      if not link(copt + ' -shared',c_,tmp,implementationdir,'shared.so',files): continue

      # use files rather than shared.so to be able to access hidden symbols
      if not link(copt,c_,tmp,implementationdir,'cycles',['cycles.o'] + files + trylibs): continue

      cycles = run(copt,c_,tmp,implementationdir,'cycles')
      try:
        cycles = int(cycles)
        if not abi in cyclesimpl: cyclesimpl[abi] = []
        cyclesimpl[abi] += [(cycles,implementationdir,c,files,copt,libs,trylibs)]
        lognow('%s cycles %d %s' % (implementationdir,cycles,c))
      except:
        continue

  for abi in abis:
    if not abi in cyclesimpl: cyclesimpl[abi] = []
    cyclesimpl[abi].sort()

    ok = False

    for cycles,implementationdir,c,files,copt,libs,trylibs in cyclesimpl[abi]:
      c_ = re.sub(' ','_',c)

      shutil.rmtree(tmp,True)
      shutil.copytree('%s/%s/%s' % (compiled,c_,implementationdir),tmp)

      lognow('%s/works testing %s' % (implementationdir,c))
      if not link(copt,c_,tmp,implementationdir,'works',['works.o'] + files + trylibs): continue

      out = run(copt,c_,tmp,implementationdir,'works')
      if out == None: continue

      lognow('selected %s %s %s %s' % (abi,implementationdir,cycles,c))
      c_ = re.sub(' ','_',c)

      for f in files:
        addlibproject(abi,'%s/%s' % (tmp,f),re.sub('/','_','%s_%s' % (implementationdir,f)))

      writefile('%s/%s/compiler' % (installlib,abi),c + '\n')

      ok = True
      break

    if not ok:
      lognow('selectfails %s %s no working implementation' % (abi,o))

# ----- shared library

abishared = set()

for c in compilers['c']:
  c = c.strip()
  if c == '': continue
  c_ = re.sub(' ','_',c)
  abi = compilerabi[c]

  if abi in abishared: continue

  if not os.path.exists('%s/%s' % (objproject,abi)): continue

  shutil.rmtree(tmp,True)
  shutil.copytree('%s/%s' % (objproject,abi),tmp)

  copt = c
  copt += ' -shared'
  copt += ' -Wl,-soname,lib%s.so.1' % project

  if link(copt,c_,tmp,'shared','lib%s.so.1' % project,sorted(os.listdir(tmp)) + syslibs[c]):
    shutil.copy('%s/lib%s.so.1' % (tmp,project),'%s/%s/' % (installlib,abi))
    os.symlink('lib%s.so.1' % project,'%s/%s/lib%s.so' % (installlib,abi,project))
    abishared.add(abi)

# ----- command

for t in types:
  t = t.strip()
  if t == '': continue
  if not os.path.isdir(t): continue
  cmd = '%s-speed' % t

  for c in compilers['c']:
    c = c.strip()
    if c == '': continue
    c_ = re.sub(' ','_',c)
    abi = compilerabi[c]

    dir = '%s/%s/%s' % (compiled,c_,'command')

    if not os.path.exists('%s/%s.o' % (dir,cmd)): continue
    if not os.path.exists('%s/limits.o' % dir): continue

    lognow('command/%s linking %s' % (cmd,c))

    copt = c
    copt += ' -L%s/%s' % (installlib,abi)
    copt += ' -Wl,-rpath=%s/%s' % (rpath,abi)
    libs = []
    libs += ['-l%s' % project]
    libs += syslibs[c]

    shutil.rmtree(tmp,True)
    os.mkdir(tmp)
    shutil.copy('%s/%s.o' % (dir,cmd),tmp)
    shutil.copy('%s/limits.o' % dir,tmp)

    if link(copt,c_,tmp,'command',cmd,['%s.o' % cmd,'limits.o'] + libs):
      copymkdir('%s/%s' % (tmp,cmd),command)
      os.chmod('%s/%s' % (command,cmd),0o711)
      break

# ----- finishing

shutil.rmtree(tmp,True)
lognow('test finishing successfully')
