#!/usr/bin/env python # vimspector - A multi-language debugging system for Vim # Copyright 2019 Ben Jackson # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. try: import urllib.request as urllib2 except ImportError: import urllib2 import argparse import contextlib import os import string import zipfile import shutil import subprocess import traceback import tarfile import hashlib import sys import json # Include vimspector source, for utils sys.path.insert( 1, os.path.join( os.path.dirname( __file__ ), 'python3' ) ) from vimspector import install GADGETS = { 'vscode-cpptools': { 'language': 'c', 'download': { 'url': 'https://github.com/Microsoft/vscode-cpptools/releases/download/' '${version}/${file_name}', }, 'do': lambda name, root: InstallCppTools( name, root ), 'all': { 'version': '0.23.1', }, 'linux': { 'file_name': 'cpptools-linux.vsix', 'checksum': 'c0f424bd6d5e016d70126587c80b92d981729c708ce524f2cce4c3f524b41d71' }, 'macos': { 'file_name': 'cpptools-osx.vsix', 'checksum': '431692395ba243ea20428e083d5df3201a0dbda31a66eab7729da0f377def5fd', }, 'windows': { 'file_name': 'cpptools-win32.vsix', 'checksum': None, }, "adapters": { "vscode-cpptools": { "name": "cppdbg", "command": [ "${gadgetDir}/vscode-cpptools/debugAdapters/OpenDebugAD7" ], "attach": { "pidProperty": "processId", "pidSelect": "ask" }, }, }, }, 'vscode-python': { 'language': 'python', 'download': { 'url': 'https://github.com/Microsoft/vscode-python/releases/download/' '${version}/${file_name}', }, 'all': { 'version': '2019.5.17059', 'file_name': 'ms-python-release.vsix', 'checksum': 'db31c9d835318209f4b26948db8b7c68b45ca4c341f6c17bb8e62dfc32f0b78d', }, 'adapters': { "vscode-python": { "name": "vscode-python", "command": [ "node", "${gadgetDir}/vscode-python/out/client/debugger/debugAdapter/main.js", ], } }, }, 'tclpro': { 'language': 'tcl', 'repo': { 'url': 'https://github.com/puremourning/TclProDebug', 'ref': 'master', }, 'do': lambda name, root: InstallTclProDebug( name, root ) }, 'netcoredbg': { 'language': 'csharp', 'enabled': False, 'download': { 'url': 'https://github.com/Samsung/netcoredbg/releases/download/latest/' '${file_name}', 'format': 'tar', }, 'all': { 'version': 'master' }, 'macos': { 'file_name': 'netcoredbg-osx-master.tar.gz', 'checksum': '', }, 'linux': { 'file_name': 'netcoredbg-linux-master.tar.gz', 'checksum': '', }, 'do': lambda name, root: MakeSymlink( gadget_dir, name, os.path.join( root, 'netcoredbg' ) ), 'adapters': { 'netcoredbg': { "name": "netcoredbg", "command": [ "${gadgetDir}/netcoredbg/netcoredbg", "--interpreter=vscode" ], "attach": { "pidProperty": "processId", "pidSelect": "ask" }, }, } }, 'vscode-mono-debug': { 'language': 'csharp', 'enabled': False, 'download': { 'url': 'https://marketplace.visualstudio.com/_apis/public/gallery/' 'publishers/ms-vscode/vsextensions/mono-debug/${version}/' 'vspackage', 'target': 'vscode-mono-debug.tar.gz', 'format': 'tar', }, 'all': { 'file_name': 'vscode-mono-debug.vsix', 'version': '0.15.8', 'checksum': '723eb2b621b99d65a24f215cb64b45f5fe694105613a900a03c859a62a810470', }, 'adapters': { 'vscode-mono-debug': { "name": "mono-debug", "command": [ "mono", "${gadgetDir}/vscode-mono-debug/bin/Release/mono-debug.exe" ], "attach": { "pidSelect": "none" }, }, } }, 'vscode-bash-debug': { 'language': 'bash', 'download': { 'url': 'https://github.com/rogalmic/vscode-bash-debug/releases/' 'download/${version}/${file_name}', }, 'all': { 'file_name': 'bash-debug-0.3.5.vsix', 'version': 'v0.3.5', 'checksum': '', } }, 'vscode-go': { 'language': 'go', 'download': { 'url': 'https://github.com/microsoft/vscode-go/releases/download/' '${version}/${file_name}' }, 'all': { 'version': '0.11.4', 'file_name': 'Go-0.11.4.vsix', 'checksum': 'ff7d7b944da5448974cb3a0086f4a2fd48e2086742d9c013d6964283d416027e' }, 'adapters': { 'vscode-go': { 'name': 'delve', 'command': [ 'node', '${gadgetDir}/vscode-go/out/src/debugAdapter/goDebug.js' ], }, }, }, 'vscode-node-debug2': { 'language': 'node', 'enabled': False, 'repo': { 'url': 'https://github.com/microsoft/vscode-node-debug2', 'ref': 'v1.39.1', }, 'do': lambda name, root: InstallNodeDebug( name, root ), 'adapters': { 'vscode-node': { 'name': 'node2', 'type': 'node2', 'command': [ 'node', '${gadgetDir}/vscode-node-debug2/out/src/nodeDebug.js' ] }, }, }, } @contextlib.contextmanager def CurrentWorkingDir( d ): cur_d = os.getcwd() try: os.chdir( d ) yield finally: os.chdir( cur_d ) def MakeExecutable( file_path ): # TODO: import stat and use them by _just_ adding the X bit. print( 'Making executable: {}'.format( file_path ) ) os.chmod( file_path, 0o755 ) def InstallCppTools( name, root ): extension = os.path.join( root, 'extension' ) # It's hilarious, but the execute bits aren't set in the vsix. So they # actually have javascript code which does this. It's just a horrible horrible # hoke that really is not funny. MakeExecutable( os.path.join( extension, 'debugAdapters', 'OpenDebugAD7' ) ) with open( os.path.join( extension, 'package.json' ) ) as f: package = json.load( f ) runtime_dependencies = package[ 'runtimeDependencies' ] for dependency in runtime_dependencies: for binary in dependency.get( 'binaries' ): file_path = os.path.abspath( os.path.join( extension, binary ) ) if os.path.exists( file_path ): MakeExecutable( os.path.join( extension, binary ) ) MakeExtensionSymlink( name, root ) def InstallTclProDebug( name, root ): configure = [ './configure' ] if OS == 'macos': # Apple removed the headers from system frameworks because they are # determined to make life difficult. And the TCL configure scripts are super # old so don't know about this. So we do their job for them and try and find # a tclConfig.sh. # # NOTE however that in Apple's infinite wisdom, installing the "headers" in # the other location is actually broken because the paths in the # tclConfig.sh are pointing at the _old_ location. You actually do have to # run the package installation which puts the headers back in order to work. # This is why the below list is does not contain stuff from # /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform # '/Applications/Xcode.app/Contents/Developer/Platforms' # '/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System' # '/Library/Frameworks/Tcl.framework', # '/Applications/Xcode.app/Contents/Developer/Platforms' # '/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System' # '/Library/Frameworks/Tcl.framework/Versions' # '/Current', for p in [ '/usr/local/opt/tcl-tk/lib' ]: if os.path.exists( os.path.join( p, 'tclConfig.sh' ) ): configure.append( '--with-tcl=' + p ) break with CurrentWorkingDir( os.path.join( root, 'lib', 'tclparser' ) ): subprocess.check_call( configure ) subprocess.check_call( [ 'make' ] ) MakeSymlink( gadget_dir, name, root ) def InstallNodeDebug( name, root ): node_version = subprocess.check_output( [ 'node', '--version' ] ).strip() print( "Node.js version: {}".format( node_version ) ) if map( int, node_version[ 1: ].split( '.' ) ) >= [ 12, 0, 0 ]: print( "Can't install vscode-debug-node2:" ) print( "Sorry, you appear to be running node 12 or later. That's not " "compatible with the build system for this extension, and as far as " "we know, there isn't a pre-built independent package." ) print( "My advice is to install nvm, then do:" ) print( " $ nvm install --lts 10" ) print( " $ nvm use --lts 10" ) print( " $ ./install_gadget.py --enable-node ..." ) raise RuntimeError( 'Invalid node environent for node debugger' ) with CurrentWorkingDir( root ): subprocess.check_call( [ 'npm', 'install' ] ) subprocess.check_call( [ 'npm', 'run', 'build' ] ) MakeSymlink( gadget_dir, name, root ) def DownloadFileTo( url, destination, file_name = None, checksum = None ): if not file_name: file_name = url.split( '/' )[ -1 ] file_path = os.path.abspath( os.path.join( destination, file_name ) ) if not os.path.isdir( destination ): os.makedirs( destination ) if os.path.exists( file_path ): if checksum: if ValidateCheckSumSHA256( file_path, checksum ): print( "Checksum matches for {}, using it".format( file_path ) ) return file_path else: print( "Checksum doesn't match for {}, removing it".format( file_path ) ) print( "Removing existing {}".format( file_path ) ) os.remove( file_path ) r = urllib2.Request( url, headers = { 'User-Agent': 'Vimspector' } ) print( "Downloading {} to {}/{}".format( url, destination, file_name ) ) with contextlib.closing( urllib2.urlopen( r ) ) as u: with open( file_path, 'wb' ) as f: f.write( u.read() ) if checksum: if not ValidateCheckSumSHA256( file_path, checksum ): raise RuntimeError( 'Checksum for {} ({}) does not match expected {}'.format( file_path, GetChecksumSHA254( file_path ), checksum ) ) else: print( "Checksum for {}: {}".format( file_path, GetChecksumSHA254( file_path ) ) ) return file_path def GetChecksumSHA254( file_path ): with open( file_path, 'rb' ) as existing_file: return hashlib.sha256( existing_file.read() ).hexdigest() def ValidateCheckSumSHA256( file_path, checksum ): existing_sha256 = GetChecksumSHA254( file_path ) return existing_sha256 == checksum def RemoveIfExists( destination ): if os.path.exists( destination ) or os.path.islink( destination ): if os.path.islink( destination ): print( "Removing file {}".format( destination ) ) os.remove( destination ) else: print( "Removing dir {}".format( destination ) ) shutil.rmtree( destination ) # Python's ZipFile module strips execute bits from files, for no good reason # other than crappy code. Let's do it's job for it. class ModePreservingZipFile( zipfile.ZipFile ): def extract( self, member, path = None, pwd = None ): if not isinstance( member, zipfile.ZipInfo ): member = self.getinfo( member ) if path is None: path = os.getcwd() ret_val = self._extract_member( member, path, pwd ) attr = member.external_attr >> 16 os.chmod( ret_val, attr ) return ret_val def ExtractZipTo( file_path, destination, format ): print( "Extracting {} to {}".format( file_path, destination ) ) RemoveIfExists( destination ) if format == 'zip': with ModePreservingZipFile( file_path ) as f: f.extractall( path = destination ) return elif format == 'tar': try: with tarfile.open( file_path ) as f: f.extractall( path = destination ) except Exception: # There seems to a bug in python's tarfile that means it can't read some # windows-generated tar files os.makedirs( destination ) with CurrentWorkingDir( destination ): subprocess.check_call( [ 'tar', 'zxvf', file_path ] ) def MakeExtensionSymlink( name, root ): MakeSymlink( gadget_dir, name, os.path.join( root, 'extension' ) ), def MakeSymlink( in_folder, link, pointing_to ): RemoveIfExists( os.path.join( in_folder, link ) ) in_folder = os.path.abspath( in_folder ) pointing_to = os.path.relpath( os.path.abspath( pointing_to ), in_folder ) os.symlink( pointing_to, os.path.join( in_folder, link ) ) def CloneRepoTo( url, ref, destination ): RemoveIfExists( destination ) subprocess.check_call( [ 'git', 'clone', url, destination ] ) subprocess.check_call( [ 'git', '-C', destination, 'checkout', ref ] ) subprocess.check_call( [ 'git', 'submodule', 'sync', '--recursive' ] ) subprocess.check_call( [ 'git', 'submodule', 'update', '--init', '--recursive' ] ) OS = install.GetOS() gadget_dir = install.GetGadgetDir( os.path.dirname( __file__ ), OS ) print( 'OS = ' + OS ) print( 'gadget_dir = ' + gadget_dir ) parser = argparse.ArgumentParser() parser.add_argument( '--all', action = 'store_true', help = 'Enable all completers' ) done_languages = set() for name, gadget in GADGETS.items(): lang = gadget[ 'language' ] if lang in done_languages: continue done_languages.add( lang ) if not gadget.get( 'enabled', True ): parser.add_argument( '--force-enable-' + lang, action = 'store_true', help = 'Install the unsupported {} debug adapter for {} support'.format( name, lang ) ) continue parser.add_argument( '--enable-' + lang, action = 'store_true', help = 'Install the {} debug adapter for {} support'.format( name, lang ) ) parser.add_argument( '--disable-' + lang, action = 'store_true', help = 'Don\t install the {} debug adapter for {} support ' '(when supplying --all)'.format( name, lang ) ) args = parser.parse_args() failed = [] all_adapters = {} for name, gadget in GADGETS.items(): if not gadget.get( 'enabled', True ): if not getattr( args, 'force_enable_' + gadget[ 'language' ] ): continue else: if not args.all and not getattr( args, 'enable_' + gadget[ 'language' ] ): continue if getattr( args, 'disable_' + gadget[ 'language' ] ): continue try: v = {} v.update( gadget.get( 'all', {} ) ) v.update( gadget.get( OS, {} ) ) if 'download' in gadget: if 'file_name' not in v: raise RuntimeError( "Unsupported OS {} for gadget {}".format( OS, name ) ) destination = os.path.join( gadget_dir, 'download', name, v[ 'version' ] ) url = string.Template( gadget[ 'download' ][ 'url' ] ).substitute( v ) file_path = DownloadFileTo( url, destination, file_name = gadget[ 'download' ].get( 'target' ), checksum = v.get( 'checksum' ) ) root = os.path.join( destination, 'root' ) ExtractZipTo( file_path, root, format = gadget[ 'download' ].get( 'format', 'zip' ) ) elif 'repo' in gadget: url = string.Template( gadget[ 'repo' ][ 'url' ] ).substitute( v ) ref = string.Template( gadget[ 'repo' ][ 'ref' ] ).substitute( v ) destination = os.path.join( gadget_dir, 'download', name ) CloneRepoTo( url, ref, destination ) root = destination if 'do' in gadget: gadget[ 'do' ]( name, root ) else: MakeExtensionSymlink( name, root ) all_adapters.update( gadget.get( 'adapters', {} ) ) print( "Done installing {}".format( name ) ) except Exception as e: traceback.print_exc() failed.append( name ) print( "FAILED installing {}: {}".format( name, e ) ) with open( install.GetGadgetConfigFile( os.path.dirname( __file__ ) ), 'w' ) as f: json.dump( { 'adapters': all_adapters }, f, indent=2, sort_keys=True ) if failed: raise RuntimeError( 'Failed to install gadgets: {}'.format( ','.join( failed ) ) )