const { promisify } = require('util'); const exec = promisify(require('child_process').exec); const readdir = promisify(require('fs').readdir); const mac = process.platform === 'darwin'; const filetype = mac ? '.dylib' : '.so'; /* these libraries will always have dynamic dependencies, so ignore */ const ignoredLibraries = mac ? new Set([]) : new Set([ 'libalsaout.so', 'libmpris.so', 'libpipewireout.so', 'libpulseout.so', ]); /* we assume these dependencies will always be available on the user's target machine. */ const validLibraries = mac ? new Set([ '/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit', '/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices', '/System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox', '/System/Library/Frameworks/Carbon.framework/Versions/A/Carbon', '/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio', '/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation', '/System/Library/Frameworks/CoreGraphics.framework/Versions/A/CoreGraphics', '/System/Library/Frameworks/CoreMedia.framework/Versions/A/CoreMedia', '/System/Library/Frameworks/CoreVideo.framework/Versions/A/CoreVideo', '/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation', '/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration', '/usr/lib/libc++.1.dylib', '/usr/lib/libobjc.A.dylib', '/usr/lib/libSystem.B.dylib', '/usr/lib/libz.1.dylib', ]) : new Set([ '/lib/ld-linux-armhf.so.3', '/lib64/ld-linux-x86-64.so.2', '/usr/lib/arm-linux-gnueabihf/libarmmem-${PLATFORM}.so', 'libc.so.6', 'libdl.so.2', 'libgcc_s.so.1', 'libm.so.6', 'libpthread.so.0', 'librt.so.1', 'libstdc++.so.6', 'libz.so.1', 'linux-vdso.so.1', ]); let errors = 0; const path = process.argv[2]; if (!path) { console.log('\n\nusage: node scan-standalone.js \n\n'); process.exit(1); } const ls = async (leaf) => { const output = await readdir(`${path}/${leaf}`); const files = output .filter((fn) => fn.indexOf(filetype) !== -1) .filter((fn) => !ignoredLibraries.has(fn)); return files; }; const lddLinux = async (fn) => { const output = await exec(`ldd ${path}/${fn}`); const problems = output.stdout .split('\n') .map((line) => line.trim()) /* libraries resolved properly will exist in the current path; exclude */ .filter((line) => !!line && line.indexOf(path) === -1) .filter((line) => line !== 'statically linked') /* libraries are formatted as " [=> ] <(offset)>". if there's a fat arrow, just grab everything to the left of it. then, if there's still an offset, strip that as well */ .map((line) => line.split(' => ')[0].split(' ')[0].trim()) .filter((line) => !validLibraries.has(line)); return problems; }; const lddMac = async (fn) => { const output = await exec(`otool -L ${path}/${fn}`); const problems = output.stdout .split('\n') .map((line) => line.trim()) /* we want all of the libraries we compile ourself to be prefixed with the value `@rpath/`. */ .filter((line) => !!line && line.indexOf('@rpath/') === -1) /* the first line of output is the path to the library, exclude it */ .slice(1) /* libraries are formatted as " (compatibility ...)", so split and take the name */ .map((line) => line.split(' (compatibility')[0].trim()) .filter((line) => !validLibraries.has(line)); return problems; }; const ldd = async (fn) => { const problems = mac ? await lddMac(fn) : await lddLinux(fn); if (!problems.length) { console.log(`${fn}: ok`); } else { console.error(`${fn}: potential problems found`); problems.forEach((line) => console.error(` * ${line}`)); ++errors; } }; const lddDir = async (leaf) => { const files = await ls(leaf); await Promise.allSettled(files.map((fn) => ldd(`${leaf}/${fn}`))); }; const main = async () => { await ldd('musikcube'); await ldd('musikcubed'); await ldd(`libmusikcore${filetype}`); await lddDir('plugins'); await lddDir('lib'); if (errors) { console.log( `\n\n\n ***** ${errors} problematic files detected! ***** \n\n\n` ); process.exit(1); } console.log('\neverything looks good!\n'); }; main();