#!/usr/bin/env python3 """Validate the future patch in archives, using published 1.0.0 or exact HG source project references.""" import argparse, datetime, hashlib, io, json, os, shutil, subprocess, tarfile, urllib.request import xml.etree.ElementTree as ET from pathlib import Path def verify_published_packages(root, consumer, cache, project): relative = Path('src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.csproj') assert (consumer/relative).read_bytes() == (root/relative).read_bytes(), 'Unexpected source-project override' assets = json.loads((consumer/project.parent/'obj/project.assets.json').read_text()) assert all(Path(p).resolve() == cache.resolve() for p in assets['packageFolders']) proof = {'no_source_project_reference_override': True, 'packages': {}} for name in ['Client', 'Contracts']: ident = 'ZB.MOM.WW.HistorianGateway.' + name library = assets['libraries'][ident + '/1.0.0'] assert library['type'] == 'package' package = cache/ident.lower()/'1.0.0' provenance = json.loads((package/'.nupkg.metadata').read_text()) assert provenance['source'].startswith('https://gitea.dohertylan.com/api/packages/dohertj2/nuget') dll = package/'lib/net10.0'/(ident + '.dll') copied = consumer/project.parent/'bin/Release/net10.0'/(ident + '.dll') digest = hashlib.sha256(dll.read_bytes()).hexdigest() assert digest == hashlib.sha256(copied.read_bytes()).hexdigest() nuspec = ET.parse(package/(ident.lower() + '.nuspec')) assert nuspec.findtext('.//{*}version') == '1.0.0' repository = nuspec.find('.//{*}repository') proof['packages'][ident] = {'version': '1.0.0', 'asset_type': 'package', 'source': provenance['source'], 'dll_sha256': digest, 'nupkg_sha256': hashlib.sha256((package/(ident.lower() + '.1.0.0.nupkg')).read_bytes()).hexdigest(), 'repository': repository.attrib if repository is not None else None, 'test_bin_dll_matches_package': True} return proof parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--consumer-root', type=Path, required=True) parser.add_argument('--hg-root', type=Path, required=True) parser.add_argument('--hg-commit', default='e4321915ebc5e0704a68ecad21b85c1cc1c80d6b') parser.add_argument('--output', type=Path, required=True) a = parser.parse_args(); root = a.consumer_root.resolve(); out = a.output.resolve() if out.is_relative_to(root) or out.is_relative_to(a.hg_root.resolve()): raise ValueError('Use an output directory outside both checkouts so archived HG does not inherit consumer central-package settings.') out.mkdir(parents=True, exist_ok=False) patch = Path(__file__).with_name('consumer-1.0.0.patch').resolve() project = Path('tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.csproj') sha = subprocess.check_output(['git','-C',str(root),'rev-parse','HEAD'],text=True).strip() hg_sha = subprocess.check_output(['git','-C',str(a.hg_root),'rev-parse',a.hg_commit],text=True).strip() def archive(repo, rev, target): target.mkdir() data = subprocess.check_output(['git','-C',str(repo),'archive',rev]) with tarfile.open(fileobj=io.BytesIO(data)) as tar: tar.extractall(target, filter='data') versions = {} for name in ['client','contracts']: url = 'https://gitea.dohertylan.com/api/packages/dohertj2/nuget/package/zb.mom.ww.historiangateway.' + name + '/index.json' versions[name] = json.load(urllib.request.urlopen(url,timeout=20))['versions'] published = all('1.0.0' in v for v in versions.values()) metadata = {'utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'consumer_commit':sha,'hg_snapshot':hg_sha, 'mode':'published-1.0.0' if published else 'unreleased-source-project-reference', 'feed_versions':versions,'patch_sha256':hashlib.sha256(patch.read_bytes()).hexdigest(), 'published_package_verification':published,'normal_package_cache_modified':False} (out/'metadata.json').write_text(json.dumps(metadata,indent=2)) consumer = out/'consumer'; archive(root,sha,consumer) subprocess.run(['git','init','-q',str(consumer)],check=True) subprocess.run(['git','-C',str(consumer),'apply',str(patch)],check=True) if not published: hg = out/'historiangw-source'; archive(a.hg_root,hg_sha,hg) adapter = consumer/'src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.csproj' tree = ET.parse(adapter) removed = 0 for group in tree.getroot().findall('ItemGroup'): for item in list(group): if item.tag=='PackageReference' and item.get('Include') in ['ZB.MOM.WW.HistorianGateway.Client','ZB.MOM.WW.HistorianGateway.Contracts']: group.remove(item);removed += 1 assert removed==2 group = ET.SubElement(tree.getroot(),'ItemGroup') for relative in ['clients/dotnet/ZB.MOM.WW.HistorianGateway.Client/ZB.MOM.WW.HistorianGateway.Client.csproj', 'src/ZB.MOM.WW.HistorianGateway.Contracts/ZB.MOM.WW.HistorianGateway.Contracts.csproj']: ET.SubElement(group,'ProjectReference',Include=str(hg/relative)) tree.write(adapter,encoding='unicode') metadata['temporary_override']='Only archived Gateway csproj: two package references replaced by exact archived HG project references; no package packed or published.' # Copy cached dependencies independently, preferably APFS copy-on-write, into the isolated package root. cache = out/'packages'; cache.mkdir() assets = json.loads((root/project.parent/'obj/project.assets.json').read_text()) normal = Path(next(iter(assets['packageFolders']))) for library in assets['libraries'].values(): if library.get('type')!='package':continue relative = library['path']; source=normal/relative; target=cache/relative target.parent.mkdir(parents=True,exist_ok=True) if target.exists():continue r=subprocess.run(['cp','-cR',str(source),str(target)],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) if r.returncode: shutil.copytree(source,target) config = ET.parse(root/'NuGet.config') for source in config.getroot().find('packageSources'): if source.get('key')=='local-mxgw': local = root/'nuget-packages' if not local.exists():local=out/'empty-local';local.mkdir() source.set('value',str(local)) config.write(out/'NuGet.config',encoding='unicode') env=dict(os.environ,NUGET_PACKAGES=str(cache),NUGET_HTTP_CACHE_PATH=str(out/'http-cache'),DOTNET_CLI_HOME=str(out/'dotnet-home'), DOTNET_SKIP_FIRST_TIME_EXPERIENCE='1',DOTNET_GENERATE_ASPNET_CERTIFICATE='false',DOTNET_CLI_TELEMETRY_OPTOUT='1') commands=[] def run(args,name): with (out/name).open('w') as log:r=subprocess.run(args,cwd=consumer,env=env,stdout=log,stderr=subprocess.STDOUT) commands.append({'argv':args,'exit':r.returncode,'log':name}) (out/'commands.json').write_text(json.dumps(commands,indent=2));print(name,'exit',r.returncode,flush=True) return r.returncode restore = run(['dotnet','restore',str(project),'--configfile',str(out/'NuGet.config'),'--packages',str(cache)],'restore.log') metadata['restore_exit']=restore if restore: (out/'metadata.json').write_text(json.dumps(metadata,indent=2));raise SystemExit(restore) results=out/'test-results' test = run(['dotnet','test',str(project),'-c','Release','--no-restore','--logger','trx','--results-directory',str(results)],'test.log') guard_env=env.copy();guard_env['MIN_EXECUTED']='1' with (out/'guard.log').open('w') as log: guard=subprocess.run(['bash',str(consumer/'scripts/ci/assert-not-all-skipped.sh'),*[str(p) for p in results.glob('*.trx')]],env=guard_env,stdout=log,stderr=subprocess.STDOUT) metadata.update({'test_exit':test,'guard_exit':guard.returncode}) ns={'t':'http://microsoft.com/schemas/VisualStudio/TeamTest/2010'} metadata['results']=[] for trx in results.glob('*.trx'): xml=ET.parse(trx);metadata['results'].append(xml.find('.//t:Counters',ns).attrib) metadata['tls_cases']=[{'name':r.get('testName'),'outcome':r.get('outcome'),'output':'\n'.join(p.text or '' for p in r.findall('.//t:StdOut',ns)), 'error':r.findtext('.//t:Message','',ns)} for r in xml.findall('.//t:UnitTestResult',ns) if 'HistorianGatewayTlsPolicyWireTests' in r.get('testName','')] if published: metadata['package_proof'] = verify_published_packages(root, consumer, cache, project) (out/'metadata.json').write_text(json.dumps(metadata,indent=2));print(json.dumps(metadata,indent=2),flush=True) raise SystemExit(test if test else guard.returncode)