1+ #!/usr/bin/env python3
2+
3+ import argparse
4+ import os
5+ import pathlib
6+ import platform
7+ import shutil
8+ import subprocess
9+ import sys
10+ import tempfile
11+ import zipfile
12+ from collections import namedtuple
13+
14+ DEPS = {"llvm" : ["LLVMSupport" ],
15+ "swift" : ["swiftFrontendTool" ]}
16+
17+
18+ def getoptions ():
19+ parser = argparse .ArgumentParser (description = "package swift for codeql compilation" )
20+ for p in DEPS :
21+ parser .add_argument (f"--{ p } " , required = True , type = resolve ,
22+ metavar = "DIR" , help = f"path to { p } build root" )
23+ default_output = f"swift-prebuilt-{ get_platform ()} .zip"
24+ parser .add_argument ("--keep-tmp-dir" , "-K" , action = "store_true" ,
25+ help = "do not clean up the temporary directory" )
26+ parser .add_argument ("--output" , "-o" , type = pathlib .Path , metavar = "DIR_OR_ZIP" ,
27+ help = "output zip file or directory "
28+ f"(by default the filename is { default_output } )" )
29+ update_help_fmt = "Only update the {} library in DIR, triggering rebuilds of required files"
30+ parser .add_argument ("--update-shared" , "-u" , metavar = "DIR" , type = pathlib .Path ,
31+ help = update_help_fmt .format ("shared" ))
32+ parser .add_argument ("--update-static" , "-U" , metavar = "DIR" , type = pathlib .Path ,
33+ help = update_help_fmt .format ("static" ))
34+ opts = parser .parse_args ()
35+ if opts .output and (opts .update_shared or opts .update_static ):
36+ parser .error ("provide --output or one of --update-*, not both" )
37+ if opts .output is None :
38+ opts .output = pathlib .Path ()
39+ opts .output = get_tgt (opts .output , default_output )
40+ return opts
41+
42+
43+ Libs = namedtuple ("Libs" , ("archive" , "static" , "shared" ))
44+
45+ DEPLIST = [x for d in DEPS .values () for x in d ]
46+
47+ CMAKELISTS_DUMMY = f"""
48+ cmake_minimum_required(VERSION 3.12.4)
49+
50+ project(dummy C CXX)
51+
52+ find_package(LLVM REQUIRED CONFIG PATHS ${{LLVM_ROOT}}/lib/cmake/llvm NO_DEFAULT_PATH)
53+ find_package(Clang REQUIRED CONFIG PATHS ${{LLVM_ROOT}}/lib/cmake/clang NO_DEFAULT_PATH)
54+ find_package(Swift REQUIRED CONFIG PATHS ${{SWIFT_ROOT}}/lib/cmake/swift NO_DEFAULT_PATH)
55+
56+ add_executable(dummy empty.cpp)
57+ target_link_libraries(dummy PRIVATE { " " .join (DEPLIST )} )
58+ """
59+
60+ EXPORTED_LIB = "swiftAndLlvmSupport"
61+
62+ CMAKELISTS_EXPORTED_FMT = """
63+ add_library({exported} INTERFACE)
64+
65+ if (BUILD_SHARED_LIBS)
66+ if (APPLE)
67+ set(EXT "dylib")
68+ else()
69+ set(EXT "so")
70+ endif()
71+ else()
72+ set(EXT "a")
73+ endif()
74+
75+ set (SwiftLLVMWrapperLib libswiftAndLlvmSupportReal.${{EXT}})
76+ set (input ${{CMAKE_CURRENT_LIST_DIR}}/${{SwiftLLVMWrapperLib}})
77+ set (output ${{CMAKE_BINARY_DIR}}/${{SwiftLLVMWrapperLib}})
78+
79+ add_custom_command(OUTPUT ${{output}}
80+ COMMAND ${{CMAKE_COMMAND}} -E copy_if_different ${{input}} ${{output}}
81+ DEPENDS ${{input}})
82+ add_custom_target(copy-llvm-swift-wrapper DEPENDS ${{output}})
83+
84+ target_include_directories({exported} INTERFACE ${{CMAKE_CURRENT_LIST_DIR}}/include)
85+ target_link_libraries({exported} INTERFACE
86+ ${{output}}
87+ {libs}
88+ )
89+ add_dependencies(swiftAndLlvmSupport copy-llvm-swift-wrapper)
90+ """
91+
92+
93+ class TempDir :
94+ def __init__ (self , cleanup = True ):
95+ self .path = None
96+ self .cleanup = cleanup
97+
98+ def __enter__ (self ):
99+ self .path = pathlib .Path (tempfile .mkdtemp ())
100+ return self .path
101+
102+ def __exit__ (self , * args ):
103+ if self .cleanup :
104+ shutil .rmtree (self .path )
105+
106+
107+ def resolve (p ):
108+ return pathlib .Path (p ).resolve ()
109+
110+
111+ def run (prog , * , cwd , env = None , input = None ):
112+ print ("running" , " " .join (prog ), f"(cwd={ cwd } )" )
113+ if env is not None :
114+ runenv = dict (os .environ )
115+ runenv .update (env )
116+ else :
117+ runenv = None
118+ subprocess .run (prog , cwd = cwd , env = runenv , input = input , text = True )
119+
120+
121+ def build (dir , targets ):
122+ print (f"building { ' ' .join (targets )} in { dir } " )
123+ cmd = ["cmake" , "--build" , "." , "--" ]
124+ cmd .extend (targets )
125+ run (cmd , cwd = dir )
126+
127+
128+ def get_platform ():
129+ return "linux" if platform .system () == "Linux" else "macos"
130+
131+
132+ def create_empty_cpp (path ):
133+ with open (path / "empty.cpp" , "w" ):
134+ pass
135+
136+
137+ def install (tmp , opts ):
138+ print ("installing dependencies" )
139+ tgt = tmp / "install"
140+ for p in DEPS :
141+ builddir = getattr (opts , p )
142+ run (["cmake" , "--build" , "." , "--" , "install" ], cwd = builddir , env = {"DESTDIR" : tgt })
143+ if sys .platform != 'linux' :
144+ return tgt / "Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain"
145+ return tgt
146+
147+
148+ def configure_dummy_project (tmp , * , llvm = None , swift = None , installed = None ):
149+ print ("configuring dummy cmake project" )
150+ if installed is not None :
151+ swift = llvm = installed / "usr"
152+ with open (tmp / "CMakeLists.txt" , "w" ) as out :
153+ out .write (CMAKELISTS_DUMMY )
154+ create_empty_cpp (tmp )
155+ tgt = tmp / "build"
156+ tgt .mkdir ()
157+ run (["cmake" , f"-DLLVM_ROOT={ llvm } " , f"-DSWIFT_ROOT={ swift } " , "-DBUILD_SHARED_LIBS=OFF" , ".." ],
158+ cwd = tgt )
159+ return tgt
160+
161+
162+ def get_libs (configured ):
163+ print ("extracting linking information from dummy project" )
164+ cut = 8
165+ if sys .platform == 'linux' :
166+ cut = 4
167+ with open (configured / "CMakeFiles" / "dummy.dir" / "link.txt" ) as link :
168+ libs = link .read ().split ()[cut :] # skip up to -o dummy
169+ ret = Libs ([], [], [])
170+ for l in libs :
171+ if l .endswith (".a" ):
172+ ret .static .append (str ((configured / l ).resolve ()))
173+ elif l .endswith (".so" ) or l .endswith (".tbd" ) or l .endswith (".dylib" ):
174+ l = pathlib .Path (l ).stem
175+ ret .shared .append (f"-l{ l [3 :]} " ) # drop 'lib' prefix and '.so' suffix
176+ elif l .startswith ("-l" ):
177+ ret .shared .append (l )
178+ else :
179+ raise ValueError (f"cannot understand link.txt: " + l )
180+ # move direct dependencies into archive
181+ ret .archive [:] = ret .static [:len (DEPLIST )]
182+ ret .static [:len (DEPLIST )] = []
183+ return ret
184+
185+
186+ def get_tgt (tgt , filename ):
187+ if tgt .is_dir ():
188+ tgt /= filename
189+ return tgt .resolve ()
190+
191+
192+ def create_static_lib (tgt , libs ):
193+ tgt = get_tgt (tgt , f"lib{ EXPORTED_LIB } Real.a" )
194+ print (f"packaging { tgt .name } " )
195+ if sys .platform == 'linux' :
196+ includedlibs = "\n " .join (f"addlib { l } " for l in libs .archive + libs .static )
197+ mriscript = f"create { tgt } \n { includedlibs } \n save\n end"
198+ run (["ar" , "-M" ], cwd = tgt .parent , input = mriscript )
199+ else :
200+ includedlibs = " " .join (f"{ l } " for l in libs .archive + libs .static )
201+ libtool_args = ["libtool" , "-static" ]
202+ libtool_args .extend (libs .archive )
203+ libtool_args .extend (libs .static )
204+ libtool_args .append ("-o" )
205+ libtool_args .append (str (tgt ))
206+ run (libtool_args , cwd = tgt .parent )
207+ return tgt
208+
209+
210+ def create_shared_lib (tgt , libs ):
211+ ext = "so"
212+ if sys .platform != 'linux' :
213+ ext = "dylib"
214+ libname = f"lib{ EXPORTED_LIB } Real.{ ext } "
215+ tgt = get_tgt (tgt , libname )
216+ print (f"packaging { libname } " )
217+ compiler = os .environ .get ("CC" , "clang" )
218+ cmd = [compiler , "-shared" ]
219+
220+ if sys .platform == 'linux' :
221+ cmd .append ("-Wl,--whole-archive" )
222+ else :
223+ cmd .append ("-Wl,-all_load" )
224+
225+ cmd .append (f"-o{ tgt } " )
226+ cmd .extend (libs .archive )
227+
228+ if sys .platform == 'linux' :
229+ cmd .append ("-Wl,--no-whole-archive" )
230+ else :
231+ cmd .append ("-lc++" )
232+
233+ cmd .extend (libs .static )
234+ cmd .extend (libs .shared )
235+ run (cmd , cwd = tgt .parent )
236+ if sys .platform != "linux" :
237+ run (["install_name_tool" , "-id" , f"@executable_path/{ libname } " , libname ], cwd = tgt .parent )
238+ return tgt
239+
240+
241+ def copy_includes (src , tgt ):
242+ print ("copying includes" )
243+ for dir , exts in (("include" , ("h" , "def" , "inc" )), ("stdlib" , ("h" ,))):
244+ srcdir = src / "usr" / dir
245+ for ext in exts :
246+ for srcfile in srcdir .rglob (f"*.{ ext } " ):
247+ tgtfile = tgt / dir / srcfile .relative_to (srcdir )
248+ tgtfile .parent .mkdir (parents = True , exist_ok = True )
249+ shutil .copy (srcfile , tgtfile )
250+
251+
252+ def create_sdk (installed , tgt ):
253+ print ("assembling sdk" )
254+ srcdir = installed / "usr" / "lib" / "swift"
255+ tgtdir = tgt / "usr" / "lib" / "swift"
256+ if get_platform () == "linux" :
257+ srcdir /= "linux"
258+ tgtdir /= "linux/x86_64"
259+ else :
260+ srcdir /= "macosx"
261+ for mod in srcdir .glob ("*.swiftmodule" ):
262+ shutil .copytree (mod , tgtdir / mod .name )
263+ shutil .copytree (installed / "usr" / "stdlib" / "public" / "SwiftShims" ,
264+ tgt / "usr" / "include" / "SwiftShims" )
265+
266+
267+ def create_export_dir (tmp , installed , libs ):
268+ print ("assembling prebuilt directory" )
269+ exportedlibs = [create_static_lib (tmp , libs ), create_shared_lib (tmp , libs )]
270+ tgt = tmp / "exported"
271+ tgt .mkdir ()
272+ for l in exportedlibs :
273+ l .rename (tgt / l .name )
274+ with open (tgt / "swift_llvm_prebuilt.cmake" , "w" ) as out :
275+ # drop -l prefix here
276+ sharedlibs = " " .join (l [2 :] for l in libs .shared )
277+ out .write (CMAKELISTS_EXPORTED_FMT .format (exported = EXPORTED_LIB , libs = sharedlibs ))
278+ copy_includes (installed , tgt )
279+ create_sdk (installed , tgt / "sdk" )
280+ return tgt
281+
282+
283+ def zip_dir (src , tgt ):
284+ print (f"compressing { src .name } to { tgt } " )
285+ tgt = get_tgt (tgt , f"swift-prebuilt-{ get_platform ()} .zip" )
286+ with zipfile .ZipFile (tgt , "w" ,
287+ compression = zipfile .ZIP_DEFLATED ,
288+ compresslevel = 6 ) as archive :
289+ for srcfile in src .rglob ("*" ):
290+ if srcfile .is_file ():
291+ print (f"deflating { srcfile .relative_to (src )} " )
292+ archive .write (srcfile , arcname = srcfile .relative_to (src ))
293+ print (f"created { tgt } " )
294+
295+
296+ def main (opts ):
297+ tmp = pathlib .Path ('/tmp/llvm-swift' )
298+ if os .path .exists (tmp ):
299+ shutil .rmtree (tmp )
300+ os .mkdir (tmp )
301+ if opts .update_shared or opts .update_static :
302+ for project , deps in DEPS .items ():
303+ build (getattr (opts , project ), deps )
304+ configured = configure_dummy_project (tmp , llvm = opts .llvm , swift = opts .swift )
305+ libs = get_libs (configured )
306+ if opts .update_shared :
307+ create_shared_lib (opts .update_shared , libs )
308+ if opts .update_static :
309+ create_static_lib (opts .update_static , libs )
310+ else :
311+ installed = install (tmp , opts )
312+ swift_syntax_build = opts .swift / "include/swift/Syntax/"
313+ swift_syntax_install = installed / "usr/include/swift/Syntax/"
314+ for header in os .listdir (swift_syntax_build ):
315+ if header .endswith ('.h' ) or header .endswith ('.def' ):
316+ shutil .copy (swift_syntax_build / header , swift_syntax_install / header )
317+ configured = configure_dummy_project (tmp , installed = installed )
318+ libs = get_libs (configured )
319+ exported = create_export_dir (tmp , installed , libs )
320+ zip_dir (exported , opts .output )
321+
322+
323+ if __name__ == "__main__" :
324+ main (getoptions ())
0 commit comments