| //===----------------------------------------------------------------------===// |
| // This source file is part of the Swift.org open source project |
| // |
| // Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors |
| // Licensed under Apache License v2.0 with Runtime Library Exception |
| // |
| // See https://swift.org/LICENSE.txt for license information |
| // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| // |
| //===----------------------------------------------------------------------===// |
| |
| const char *usage = |
| " swift-stdlib-tool --print [options...]\n" |
| " Find and print the Swift libraries required by an app.\n" |
| "\n" |
| " swift-stdlib-tool --copy [options...]\n" |
| " Copy the Swift libraries into an app bundle, and optionally sign them.\n" |
| "\n" |
| " --verbose\n" |
| " Print progress.\n" |
| " --verbose --verbose\n" |
| " Print debugging details.\n" |
| " --help\n" |
| " Print usage.\n" |
| "\n" |
| " Options for lookup:\n" |
| " --scan-executable <path>\n" |
| " Scan the executable at <path> for references to Swift libraries.\n" |
| " This option may be set multiple times.\n" |
| " --scan-folder <path>\n" |
| " Scan any executables inside <path> for references to Swift libraries.\n" |
| " This option may be set multiple times.\n" |
| " --platform <macosx|iphoneos|iphonesimulator>\n" |
| " Use the Swift libraries for <platform>.\n" |
| " --source-libraries <path>\n" |
| " Search <path> for Swift libraries.\n" |
| " The default is /path/to/swift-stdlib-tool/../../lib/swift/<platform>/\n" |
| "\n" |
| " Options for copying and signing:\n" |
| " --destination <path>\n" |
| " Copy Swift libraries into <path>.\n" |
| " --unsigned-destination <path>\n" |
| " Copy Swift libraries into <path> without signing them.\n" |
| " --sign <identity>\n" |
| " Sign copied Swift libraries using <identity>.\n" |
| " --keychain <keychain>\n" |
| " Search <keychain> for the code signing identity.\n" |
| " --Xcodesign <option>\n" |
| " Pass <option> to the codesign tool.\n" |
| " --strip-bitcode\n" |
| " Remove embedded bitcode from libraries copied to --destination.\n" |
| " Libraries copied to --unsigned-destination are unmodified.\n" |
| "\n" |
| " Options for libraries copied as resources\n" |
| " --resource-library <library>\n" |
| " Copy <library> and its dependencies as resources without signing\n" |
| " them. These copies are in addition to any libraries copied as a result\n" |
| " of the --scan-executable option.\n" |
| " Any library in the Swift library search path can be specified for\n" |
| " <library>.\n" |
| " This option may be set multiple times.\n" |
| " --resource-destination <path>\n" |
| " The <path> to copy Swift resource libraries to.\n" |
| "\n" |
| ; |
| |
| #include <sys/types.h> |
| #include <sys/uio.h> |
| #include <sys/stat.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <unistd.h> |
| #include <string.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <os/overflow.h> |
| #include <mach-o/fat.h> |
| #include <mach-o/dyld.h> |
| #include <mach-o/loader.h> |
| #include <libkern/OSByteOrder.h> |
| |
| #include <algorithm> |
| using namespace std; |
| |
| #include <Foundation/Foundation.h> |
| |
| #pragma clang diagnostic ignored "-Wgcc-compat" |
| |
| #ifndef CPU_TYPE_ARM64 |
| # define CPU_TYPE_ARM64 ((cpu_type_t) (CPU_TYPE_ARM | CPU_ARCH_ABI64)) |
| #endif |
| |
| static int Verbose = 0; |
| |
| #ifdef __OPTIMIZE__ |
| #define INLINE __attribute__((always_inline)) |
| #else |
| #define INLINE |
| #endif |
| |
| // |
| // This abstraction layer is for use with file formats that have 64-bit/32-bit and Big-Endian/Little-Endian variants |
| // |
| // For example: to make a utility that handles 32-bit little enidan files use: Pointer32<LittleEndian> |
| // |
| // |
| // get16() read a 16-bit number from an E endian struct |
| // set16() write a 16-bit number to an E endian struct |
| // get32() read a 32-bit number from an E endian struct |
| // set32() write a 32-bit number to an E endian struct |
| // get64() read a 64-bit number from an E endian struct |
| // set64() write a 64-bit number to an E endian struct |
| // |
| // getBits() read a bit field from an E endian struct (bitCount=number of bits in field, firstBit=bit index of field) |
| // setBits() write a bit field to an E endian struct (bitCount=number of bits in field, firstBit=bit index of field) |
| // |
| // getBitsRaw() read a bit field from a struct with native endianness |
| // setBitsRaw() write a bit field from a struct with native endianness |
| // |
| |
| class BigEndian |
| { |
| public: |
| static uint16_t get16(const uint16_t& from) INLINE { return OSReadBigInt16(&from, 0); } |
| static void set16(uint16_t& into, uint16_t value) INLINE { OSWriteBigInt16(&into, 0, value); } |
| |
| static uint32_t get32(const uint32_t& from) INLINE { return OSReadBigInt32(&from, 0); } |
| static void set32(uint32_t& into, uint32_t value) INLINE { OSWriteBigInt32(&into, 0, value); } |
| |
| static uint64_t get64(const uint64_t& from) INLINE { return OSReadBigInt64(&from, 0); } |
| static void set64(uint64_t& into, uint64_t value) INLINE { OSWriteBigInt64(&into, 0, value); } |
| |
| static uint32_t getBits(const uint32_t& from, |
| uint8_t firstBit, uint8_t bitCount) INLINE { return getBitsRaw(get32(from), firstBit, bitCount); } |
| static void setBits(uint32_t& into, uint32_t value, |
| uint8_t firstBit, uint8_t bitCount) INLINE { uint32_t temp = get32(into); setBitsRaw(temp, value, firstBit, bitCount); set32(into, temp); } |
| |
| static uint32_t getBitsRaw(const uint32_t& from, |
| uint8_t firstBit, uint8_t bitCount) INLINE { return ((from >> (32-firstBit-bitCount)) & ((1<<bitCount)-1)); } |
| static void setBitsRaw(uint32_t& into, uint32_t value, |
| uint8_t firstBit, uint8_t bitCount) INLINE { uint32_t temp = into; |
| const uint32_t mask = ((1<<bitCount)-1); |
| temp &= ~(mask << (32-firstBit-bitCount)); |
| temp |= ((value & mask) << (32-firstBit-bitCount)); |
| into = temp; } |
| enum { little_endian = 0 }; |
| }; |
| |
| |
| class LittleEndian |
| { |
| public: |
| static uint16_t get16(const uint16_t& from) INLINE { return OSReadLittleInt16(&from, 0); } |
| static void set16(uint16_t& into, uint16_t value) INLINE { OSWriteLittleInt16(&into, 0, value); } |
| |
| static uint32_t get32(const uint32_t& from) INLINE { return OSReadLittleInt32(&from, 0); } |
| static void set32(uint32_t& into, uint32_t value) INLINE { OSWriteLittleInt32(&into, 0, value); } |
| |
| static uint64_t get64(const uint64_t& from) INLINE { return OSReadLittleInt64(&from, 0); } |
| static void set64(uint64_t& into, uint64_t value) INLINE { OSWriteLittleInt64(&into, 0, value); } |
| |
| static uint32_t getBits(const uint32_t& from, |
| uint8_t firstBit, uint8_t bitCount) INLINE { return getBitsRaw(get32(from), firstBit, bitCount); } |
| static void setBits(uint32_t& into, uint32_t value, |
| uint8_t firstBit, uint8_t bitCount) INLINE { uint32_t temp = get32(into); setBitsRaw(temp, value, firstBit, bitCount); set32(into, temp); } |
| |
| static uint32_t getBitsRaw(const uint32_t& from, |
| uint8_t firstBit, uint8_t bitCount) INLINE { return ((from >> firstBit) & ((1<<bitCount)-1)); } |
| static void setBitsRaw(uint32_t& into, uint32_t value, |
| uint8_t firstBit, uint8_t bitCount) INLINE { uint32_t temp = into; |
| const uint32_t mask = ((1<<bitCount)-1); |
| temp &= ~(mask << firstBit); |
| temp |= ((value & mask) << firstBit); |
| into = temp; } |
| enum { little_endian = 1 }; |
| }; |
| |
| #if __BIG_ENDIAN__ |
| typedef BigEndian CurrentEndian; |
| typedef LittleEndian OtherEndian; |
| #elif __LITTLE_ENDIAN__ |
| typedef LittleEndian CurrentEndian; |
| typedef BigEndian OtherEndian; |
| #else |
| #error unknown endianness |
| #endif |
| |
| |
| template <typename _E> |
| class Pointer32 |
| { |
| public: |
| typedef uint32_t uint_t; |
| typedef int32_t sint_t; |
| typedef _E E; |
| |
| static uint64_t getP(const uint_t& from) INLINE { return _E::get32(from); } |
| static void setP(uint_t& into, uint64_t value) INLINE { _E::set32(into, value); } |
| }; |
| |
| |
| template <typename _E> |
| class Pointer64 |
| { |
| public: |
| typedef uint64_t uint_t; |
| typedef int64_t sint_t; |
| typedef _E E; |
| |
| static uint64_t getP(const uint_t& from) INLINE { return _E::get64(from); } |
| static void setP(uint_t& into, uint64_t value) INLINE { _E::set64(into, value); } |
| }; |
| |
| |
| // |
| // mach-o file header |
| // |
| template <typename P> struct macho_header_content {}; |
| template <> struct macho_header_content<Pointer32<BigEndian> > { mach_header fields; }; |
| template <> struct macho_header_content<Pointer64<BigEndian> > { mach_header_64 fields; }; |
| template <> struct macho_header_content<Pointer32<LittleEndian> > { mach_header fields; }; |
| template <> struct macho_header_content<Pointer64<LittleEndian> > { mach_header_64 fields; }; |
| |
| template <typename P> |
| class macho_header { |
| public: |
| uint32_t magic() const INLINE { return E::get32(header.fields.magic); } |
| void set_magic(uint32_t value) INLINE { E::set32(header.fields.magic, value); } |
| |
| uint32_t cputype() const INLINE { return E::get32(header.fields.cputype); } |
| void set_cputype(uint32_t value) INLINE { E::set32((uint32_t&)header.fields.cputype, value); } |
| |
| uint32_t cpusubtype() const INLINE { return E::get32(header.fields.cpusubtype); } |
| void set_cpusubtype(uint32_t value) INLINE { E::set32((uint32_t&)header.fields.cpusubtype, value); } |
| |
| uint32_t filetype() const INLINE { return E::get32(header.fields.filetype); } |
| void set_filetype(uint32_t value) INLINE { E::set32(header.fields.filetype, value); } |
| |
| uint32_t ncmds() const INLINE { return E::get32(header.fields.ncmds); } |
| void set_ncmds(uint32_t value) INLINE { E::set32(header.fields.ncmds, value); } |
| |
| uint32_t sizeofcmds() const INLINE { return E::get32(header.fields.sizeofcmds); } |
| void set_sizeofcmds(uint32_t value) INLINE { E::set32(header.fields.sizeofcmds, value); } |
| |
| uint32_t flags() const INLINE { return E::get32(header.fields.flags); } |
| void set_flags(uint32_t value) INLINE { E::set32(header.fields.flags, value); } |
| |
| uint32_t reserved() const INLINE { return E::get32(header.fields.reserved); } |
| void set_reserved(uint32_t value) INLINE { E::set32(header.fields.reserved, value); } |
| |
| typedef typename P::E E; |
| private: |
| macho_header_content<P> header; |
| }; |
| |
| |
| // |
| // mach-o load command |
| // |
| template <typename P> |
| class macho_load_command { |
| public: |
| uint32_t cmd() const INLINE { return E::get32(command.cmd); } |
| void set_cmd(uint32_t value) INLINE { E::set32(command.cmd, value); } |
| |
| uint32_t cmdsize() const INLINE { return E::get32(command.cmdsize); } |
| void set_cmdsize(uint32_t value) INLINE { E::set32(command.cmdsize, value); } |
| |
| typedef typename P::E E; |
| private: |
| load_command command; |
| }; |
| |
| |
| // |
| // mach-o uuid load command |
| // |
| template <typename P> |
| class macho_uuid_command { |
| public: |
| uint32_t cmd() const INLINE { return E::get32(fields.cmd); } |
| void set_cmd(uint32_t value) INLINE { E::set32(fields.cmd, value); } |
| |
| uint32_t cmdsize() const INLINE { return E::get32(fields.cmdsize); } |
| void set_cmdsize(uint32_t value) INLINE { E::set32(fields.cmdsize, value); } |
| |
| const uint8_t* uuid() const INLINE { return fields.uuid; } |
| void set_uuid(uint8_t value[16]) INLINE { memcpy(&fields.uuid, value, 16); } |
| |
| typedef typename P::E E; |
| private: |
| uuid_command fields; |
| }; |
| |
| |
| // |
| // mach-o dylib load command |
| // |
| template <typename P> |
| class macho_dylib_command { |
| public: |
| uint32_t cmd() const INLINE { return E::get32(fields.cmd); } |
| void set_cmd(uint32_t value) INLINE { E::set32(fields.cmd, value); } |
| |
| uint32_t cmdsize() const INLINE { return E::get32(fields.cmdsize); } |
| void set_cmdsize(uint32_t value) INLINE { E::set32(fields.cmdsize, value); } |
| |
| uint32_t name_offset() const INLINE { return E::get32(fields.dylib.name.offset); } |
| void set_name_offset(uint32_t value) INLINE { E::set32(fields.dylib.name.offset, value); } |
| |
| uint32_t timestamp() const INLINE { return E::get32(fields.dylib.timestamp); } |
| void set_timestamp(uint32_t value) INLINE { E::set32(fields.dylib.timestamp, value); } |
| |
| uint32_t current_version() const INLINE { return E::get32(fields.dylib.current_version); } |
| void set_current_version(uint32_t value) INLINE { E::set32(fields.dylib.current_version, value); } |
| |
| uint32_t compatibility_version() const INLINE { return E::get32(fields.dylib.compatibility_version); } |
| void set_compatibility_version(uint32_t value) INLINE { E::set32(fields.dylib.compatibility_version, value); } |
| |
| const char* name() const INLINE { return (const char*)&fields + name_offset(); } |
| void set_name_offset() INLINE { set_name_offset(sizeof(fields)); } |
| |
| typedef typename P::E E; |
| private: |
| dylib_command fields; |
| }; |
| |
| |
| // Print everything to standard output. |
| // Mixing stdout and stderr looks bad when the output is reprinted by Xcode. |
| |
| void printUsage() |
| { |
| fprintf(stdout, "%s", usage); |
| } |
| |
| void fail(const char *msg, ...) |
| __attribute__((format(printf, 1, 2))) __attribute__((noreturn)) |
| { |
| va_list args; |
| va_start(args, msg); |
| char *msg2; |
| asprintf(&msg2, "*** error: %s\n", msg); |
| vfprintf(stdout, msg2, args); |
| exit(1); |
| } |
| |
| void fail_errno(const char *msg, ...) |
| __attribute__((format(printf, 1, 2))) __attribute__((noreturn)) |
| { |
| va_list args; |
| va_start(args, msg); |
| char *msg2; |
| asprintf(&msg2, "*** error: %s: %s\n", msg, strerror(errno)); |
| vfprintf(stdout, msg2, args); |
| exit(1); |
| } |
| |
| void fail_usage(const char *msg, ...) |
| __attribute__((format(printf, 1, 2))) __attribute__((noreturn)) |
| { |
| va_list args; |
| va_start(args, msg); |
| char *msg2; |
| asprintf(&msg2, "*** error: %s\n\n", msg); |
| vfprintf(stdout, msg2, args); |
| printUsage(); |
| exit(1); |
| } |
| |
| |
| void log_vn(int verbosity, const char *msg, va_list args) |
| __attribute__((format(printf, 2, 0))) |
| { |
| if (verbosity <= Verbose) { |
| char *msg2; |
| asprintf(&msg2, "%s\n", msg); |
| vfprintf(stdout, msg2, args); |
| free(msg2); |
| } |
| } |
| |
| int log_v(const char *msg, ...) |
| __attribute__((format(printf, 1, 2))) |
| { |
| va_list args; |
| va_start(args, msg); |
| log_vn(1, msg, args); |
| return -1; |
| } |
| |
| int log_vv(const char *msg, ...) |
| __attribute__((format(printf, 1, 2))) |
| { |
| va_list args; |
| va_start(args, msg); |
| log_vn(2, msg, args); |
| return -1; |
| } |
| |
| |
| ssize_t pread_all(int fd, void *buf, size_t count, off_t offset) |
| { |
| size_t total = 0; |
| while (total < count) { |
| ssize_t readed = pread(fd, (void *)((char *)buf+total), |
| count-total, offset+total); |
| if (readed > 0) total += readed; // got data |
| else if (readed == 0) return total; // EOF: done |
| else if (readed == -1 && errno != EINTR) return -1; |
| // error but not EINTR: fail |
| } |
| |
| return total; |
| } |
| |
| |
| template <typename T> |
| int parse_macho(int fd, uint32_t offset, uint32_t size, |
| void (^dylibVisitor)(NSString *path), |
| void (^uuidVisitor)(NSUUID *uuid)) |
| { |
| ssize_t readed; |
| |
| macho_header<T> mh; |
| if (size < sizeof(mh)) return log_vv("file is too small"); |
| readed = pread_all(fd, &mh, sizeof(mh), offset); |
| if (readed != sizeof(mh)) return log_vv("pread failed"); |
| |
| uint32_t sizeofcmds = mh.sizeofcmds(); |
| size -= sizeof(mh); |
| offset += sizeof(mh); |
| if (size < sizeofcmds) return log_vv("file is badly formed"); |
| |
| uint8_t *cmdp = (uint8_t *)malloc(sizeofcmds); |
| if (!cmdp) return log_vv("malloc(sizeofcmds) failed"); |
| |
| readed = pread_all(fd, cmdp, sizeofcmds, offset); |
| if (readed == sizeofcmds) { |
| uint8_t *cmds = cmdp; |
| for (uint32_t c = 0; c < mh.ncmds(); c++) { |
| macho_load_command<T> *cmd; |
| if (size < sizeof(*cmd)) return log_vv("file is badly formed"); |
| cmd = (macho_load_command<T>*) cmds; |
| if (size < cmd->cmdsize()) return log_vv("file is badly formed"); |
| cmds += cmd->cmdsize(); |
| size -= cmd->cmdsize(); |
| |
| if (dylibVisitor && |
| (cmd->cmd() == LC_LOAD_DYLIB || |
| cmd->cmd() == LC_LOAD_WEAK_DYLIB || |
| cmd->cmd() == LC_LAZY_LOAD_DYLIB)) |
| { |
| macho_dylib_command<T>* dylib = (macho_dylib_command<T>*)cmd; |
| if (dylib->cmdsize() < dylib->name_offset()) continue; |
| char *name = (char *)dylib + dylib->name_offset(); |
| size_t name_len = |
| strnlen(name, dylib->cmdsize() - dylib->name_offset()); |
| log_vv(" loads %.*s", (int)name_len, name); |
| |
| #define PREPREFIX "@rpath/" |
| #define PREFIX PREPREFIX "libswift" |
| if (0 == strncmp(name, PREFIX, strlen(PREFIX))) { |
| NSString *nsname = |
| [[[[NSString alloc] initWithBytes:name length:name_len |
| encoding:NSUTF8StringEncoding] autorelease] |
| substringFromIndex:strlen(PREPREFIX)]; |
| if (nsname) dylibVisitor(nsname); |
| } |
| } |
| else if (uuidVisitor && cmd->cmd() == LC_UUID) { |
| macho_uuid_command<T>* uuid_cmd = (macho_uuid_command<T>*)cmd; |
| if (uuid_cmd->cmdsize() < sizeof(uuid_command)) continue; |
| CFUUIDBytes uuid_bytes = *(const CFUUIDBytes *)uuid_cmd->uuid(); |
| NSUUID *uuid = (NSUUID *) |
| CFUUIDCreateFromUUIDBytes(nil, uuid_bytes); |
| uuidVisitor(uuid); |
| [uuid release]; |
| } |
| } |
| } |
| free(cmdp); |
| |
| return 0; |
| } |
| |
| |
| int parse_macho(int fd, uint32_t offset, uint32_t size, |
| void (^dylibVisitor)(NSString *path), |
| void (^uuidVisitor)(NSUUID *uuid)) |
| { |
| uint32_t magic; |
| if (size < sizeof(magic)) return log_vv("file is too small"); |
| ssize_t readed = pread_all(fd, &magic, sizeof(magic), offset); |
| if (readed != sizeof(magic)) return log_vv("pread failed"); |
| |
| switch (magic) { |
| case MH_MAGIC_64: |
| return parse_macho<Pointer64<CurrentEndian>>(fd, offset, size, |
| dylibVisitor, uuidVisitor); |
| case MH_MAGIC: |
| return parse_macho<Pointer32<CurrentEndian>>(fd, offset, size, |
| dylibVisitor, uuidVisitor); |
| case MH_CIGAM_64: |
| return parse_macho<Pointer64<OtherEndian>>(fd, offset, size, |
| dylibVisitor, uuidVisitor); |
| case MH_CIGAM: |
| return parse_macho<Pointer32<OtherEndian>>(fd, offset, size, |
| dylibVisitor, uuidVisitor); |
| default: |
| return log_vv("file is not mach-o"); |
| } |
| } |
| |
| |
| int parse_fat(int fd, off_t fsize, char *buffer, size_t size, |
| void (^dylibVisitor)(NSString *path), |
| void (^uuidVisitor)(NSUUID *uuid)) |
| { |
| uint32_t magic; |
| |
| if (size < sizeof(magic)) { |
| return log_vv("file is too small"); |
| } |
| |
| magic = *(uint32_t *)buffer; |
| if (magic == FAT_MAGIC || magic == FAT_CIGAM) { |
| struct fat_header *fh; |
| uint32_t fat_magic, fat_nfat_arch; |
| struct fat_arch *archs; |
| uint32_t i; |
| |
| if (size < sizeof(struct fat_header)) { |
| return log_vv("file is too small"); |
| } |
| |
| fh = (struct fat_header *)buffer; |
| fat_magic = OSSwapBigToHostInt32(fh->magic); |
| fat_nfat_arch = OSSwapBigToHostInt32(fh->nfat_arch); |
| |
| size_t fat_arch_size; |
| // fat_nfat_arch * sizeof(struct fat_arch) + sizeof(struct fat_header) |
| if (os_mul_and_add_overflow(fat_nfat_arch, sizeof(struct fat_arch), |
| sizeof(struct fat_header), &fat_arch_size)) |
| { |
| return log_vv("too many fat archs\n"); |
| } |
| if (size < fat_arch_size) { |
| return log_vv("file is too small"); |
| } |
| |
| archs = (struct fat_arch *)(buffer + sizeof(struct fat_header)); |
| |
| /* Special case hidden CPU_TYPE_ARM64 */ |
| size_t fat_arch_plus_one_size; |
| if (os_add_overflow(fat_arch_size, sizeof(struct fat_arch), |
| &fat_arch_plus_one_size)) |
| { |
| return log_vv("too many fat archs\n"); |
| } |
| if (size >= fat_arch_plus_one_size) { |
| if (fat_nfat_arch > 0 |
| && OSSwapBigToHostInt32(archs[fat_nfat_arch].cputype) == CPU_TYPE_ARM64) { |
| fat_nfat_arch++; |
| } |
| } |
| /* End special case hidden CPU_TYPE_ARM64 */ |
| |
| for (i=0; i < fat_nfat_arch; i++) { |
| int ret; |
| uint32_t arch_cputype, arch_cpusubtype, arch_offset, arch_size, arch_align; |
| |
| arch_cputype = OSSwapBigToHostInt32(archs[i].cputype); |
| arch_cpusubtype = OSSwapBigToHostInt32(archs[i].cpusubtype); |
| arch_offset = OSSwapBigToHostInt32(archs[i].offset); |
| arch_size = OSSwapBigToHostInt32(archs[i].size); |
| arch_align = OSSwapBigToHostInt32(archs[i].align); |
| |
| /* Check that slice data is after all fat headers and archs */ |
| if (arch_offset < fat_arch_size) { |
| return log_vv("file is badly formed"); |
| } |
| |
| /* Check that the slice ends before the file does */ |
| if (arch_offset > fsize) { |
| return log_vv("file is badly formed"); |
| } |
| |
| if (arch_size > fsize) { |
| return log_vv("file is badly formed"); |
| } |
| |
| if (arch_offset > (fsize - arch_size)) { |
| return log_vv("file is badly formed"); |
| } |
| |
| ret = parse_macho(fd, arch_offset, arch_size, |
| dylibVisitor, uuidVisitor); |
| if (ret != 0) { |
| return ret; |
| } |
| } |
| return 0; |
| } else { |
| /* Not a fat file */ |
| return parse_macho(fd, 0, fsize, dylibVisitor, uuidVisitor); |
| } |
| } |
| |
| |
| void process(NSString *path, void(^dylibVisitor)(NSString *), |
| void (^uuidVisitor)(NSUUID *)) |
| { |
| log_vv("Scanning %s...", path.fileSystemRepresentation); |
| |
| int fd = open(path.fileSystemRepresentation, O_RDONLY); |
| if (fd < 0) log_vv("%s: open failed: %s", |
| path.fileSystemRepresentation, strerror(errno)); |
| |
| struct stat st; |
| if (fstat(fd, &st) < 0) { |
| log_vv("%s: stat failed: %s", |
| path.fileSystemRepresentation, strerror(errno)); |
| } else { |
| const int len = 4096; |
| char buf[len]; |
| ssize_t readed = pread_all(fd, buf, len, 0); |
| if (readed != len) { |
| log_vv("%s: pread failed: %s", |
| path.fileSystemRepresentation, strerror(errno)); |
| } else { |
| parse_fat(fd, st.st_size, buf, len, dylibVisitor, uuidVisitor); |
| } |
| } |
| close(fd); |
| } |
| |
| |
| @implementation NSString (sst) |
| -(NSString *)sst_stringByAppendingPathComponents:(NSArray *)components |
| { |
| NSString *result = self; |
| @autoreleasepool { |
| for (NSString *component in components) { |
| result = [result stringByAppendingPathComponent:component]; |
| } |
| [result retain]; |
| } |
| return [result autorelease]; |
| } |
| @end |
| |
| @implementation NSTask (sst) |
| -(NSString *)sst_command { |
| NSMutableString *command = [self.launchPath mutableCopy]; |
| for (NSString *arg in self.arguments) { |
| [command appendFormat:@" '%@'", arg]; |
| } |
| return command; |
| } |
| @end |
| |
| |
| bool operator <= (const struct timespec &lhs, const struct timespec &rhs) |
| { |
| if (lhs.tv_sec == rhs.tv_sec) return lhs.tv_nsec <= rhs.tv_nsec; |
| return lhs.tv_sec <= rhs.tv_sec; |
| } |
| |
| |
| // This executable's own path. |
| NSString *self_executable = []() -> NSString * { |
| uint32_t len = 0; |
| _NSGetExecutablePath(nil, &len); |
| char buf[len]; |
| _NSGetExecutablePath(buf, &len); |
| return [[NSString alloc] initWithUTF8String:buf]; |
| }(); |
| |
| |
| // This executable's own xctoolchain path. |
| NSString *self_toolchain = []() -> NSString * { |
| @autoreleasepool { |
| NSString *result = self_executable; |
| |
| // Remove the executable name. |
| result = result.stringByDeletingLastPathComponent; |
| |
| // Remove trailing /usr/bin, if any |
| if ([result.lastPathComponent isEqualToString:@"bin"]) { |
| result = result.stringByDeletingLastPathComponent; |
| } |
| if ([result.lastPathComponent isEqualToString:@"usr"]) { |
| result = result.stringByDeletingLastPathComponent; |
| } |
| return [result retain]; |
| } |
| }(); |
| |
| |
| // Runs a tool with `xcrun`. |
| // Returns NSTask.terminationStatus. |
| // Prints the tool's command line if we are verbose. |
| // Prints the tool's stdout and stderr if terminationStatus is non-zero |
| // or if we are very verbose. |
| typedef void (^XcrunToolBlock)(NSData *stdOutData, NSData *stdErrorData, int err); |
| |
| int xcrunToolCommand(NSArray *commandAndArguments, XcrunToolBlock block = 0) |
| { |
| @autoreleasepool { |
| NSTask *task = [[[NSTask alloc] init] autorelease]; |
| task.launchPath = @"/usr/bin/xcrun"; |
| |
| // Tell xcrun to search our toolchain first. |
| NSMutableArray *arguments = |
| [[commandAndArguments mutableCopy] autorelease]; |
| [arguments insertObject:@"--toolchain" atIndex:0]; |
| [arguments insertObject:self_toolchain atIndex:1]; |
| |
| // Tell xcrun to print its command if we are very verbose. |
| if (Verbose > 1) { |
| [arguments insertObject:@"--log" atIndex:2]; |
| } |
| |
| task.arguments = arguments; |
| |
| NSPipe *outPipe = [NSPipe pipe]; |
| NSPipe *errPipe = [NSPipe pipe]; |
| task.standardOutput = outPipe; |
| task.standardError = errPipe; |
| |
| log_v(" %s", task.sst_command.fileSystemRepresentation); |
| [task launch]; |
| |
| // Read stdout and stderr in parallel, then wait for the task |
| // to exit. Anything else risks deadlock if the task fills |
| // one of the output buffers. |
| |
| __block NSData *stdErrData = nil; |
| dispatch_semaphore_t gotStdErr = dispatch_semaphore_create(0); |
| dispatch_queue_t concurrentQueue = |
| dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
| dispatch_async(concurrentQueue, ^{ |
| stdErrData = |
| [[errPipe.fileHandleForReading readDataToEndOfFile] retain]; |
| dispatch_semaphore_signal(gotStdErr); |
| }); |
| |
| NSData *stdOutData = [outPipe.fileHandleForReading readDataToEndOfFile]; |
| |
| dispatch_semaphore_wait(gotStdErr, DISPATCH_TIME_FOREVER); |
| dispatch_release(gotStdErr); |
| [stdErrData autorelease]; |
| |
| [task waitUntilExit]; |
| |
| // Task is finished and we have its stdout and stderr output. |
| |
| // Print its stdout and stderr if it failed or we are verbose. |
| // (Print nothing by default because codesign is noisy.) |
| |
| int err = task.terminationStatus; |
| if (err || Verbose > 1) { |
| fwrite(stdErrData.bytes, stdErrData.length, 1, stdout); |
| fwrite(stdOutData.bytes, stdOutData.length, 1, stdout); |
| } |
| if (block) { block(stdOutData, stdErrData, err); } |
| return err; |
| } |
| } |
| |
| |
| void copyAndStripBitcode(NSString *src, NSString *dst) |
| { |
| // -r removes bitcode |
| int err = xcrunToolCommand(@[@"bitcode_strip", src, @"-r", @"-o", dst]); |
| |
| // Fail if bitcode_strip failed. |
| if (err) { |
| fail("Couldn't copy and strip bitcode %s to %s: bitcode_strip failed " |
| "with exit code %d", |
| src.fileSystemRepresentation, dst.fileSystemRepresentation, err); |
| } |
| } |
| |
| |
| void |
| copyFile(NSFileManager *fm, NSString *src, NSString *dst, bool stripBitcode) |
| { |
| if (stripBitcode) { |
| copyAndStripBitcode(src, dst); |
| } else { |
| NSError *nserr; |
| if (![fm copyItemAtPath:src toPath:dst error:&nserr]) { |
| fail("Couldn't copy %s to %s: %s", |
| src.fileSystemRepresentation, |
| dst.fileSystemRepresentation, |
| nserr.localizedFailureReason.UTF8String); |
| } |
| } |
| } |
| |
| |
| void copyLibraries(NSString *src_dir, NSString *dst_dir, |
| NSMutableDictionary *libs, bool stripBitcode) |
| { |
| NSFileManager *fm = NSFileManager.defaultManager; |
| |
| [fm createDirectoryAtPath: dst_dir withIntermediateDirectories:YES |
| attributes:nil error:nil]; |
| |
| for (NSString *lib in libs) @autoreleasepool { |
| NSString *src = [src_dir stringByAppendingPathComponent:lib]; |
| NSString *dst = [dst_dir stringByAppendingPathComponent:lib]; |
| |
| // Compare UUIDs of src and dst and don't copy if they're the same. |
| // Do not use mod times for this task: the dst copy gets code-signed |
| // and bitcode-stripped so it can look newer than it really is. |
| NSSet *srcUUIDs = libs[lib]; |
| NSMutableSet *dstUUIDs = [NSMutableSet set]; |
| process(dst, nil, ^(NSUUID *uuid) { |
| [dstUUIDs addObject:uuid]; |
| }); |
| |
| log_vv("Source UUIDs %s: %s", src.fileSystemRepresentation, |
| srcUUIDs.description.UTF8String); |
| log_vv("Destination UUIDs %s: %s", dst.fileSystemRepresentation, |
| dstUUIDs.description.UTF8String); |
| |
| if ([srcUUIDs isEqualToSet:dstUUIDs]) { |
| log_v("%s is up to date at %s", |
| lib.fileSystemRepresentation, dst.fileSystemRepresentation); |
| continue; |
| } |
| |
| // Perform the copy. |
| |
| log_v("Copying %s from %s to %s", |
| lib.fileSystemRepresentation, |
| src_dir.fileSystemRepresentation, |
| dst_dir.fileSystemRepresentation); |
| |
| [fm removeItemAtPath:dst error:nil]; // fixme report this err? |
| copyFile(fm, src.stringByResolvingSymlinksInPath, dst, stripBitcode); |
| } |
| } |
| |
| NSString *relative_path(NSString *base, NSString *suffix_name) |
| { |
| NSDictionary *env = NSProcessInfo.processInfo.environment; |
| NSString *suffix = env[suffix_name]; |
| if (base && suffix) { |
| return [base stringByAppendingPathComponent:suffix]; |
| } |
| return nil; |
| } |
| |
| void add_relative_path(NSMutableArray *paths, |
| NSString *base, NSString *suffix_name) |
| { |
| NSString *path = relative_path(base, suffix_name); |
| if (path) [paths addObject:path]; |
| } |
| |
| NSData *query_code_signature(NSString *file) { |
| __block NSData *d = 0; |
| @autoreleasepool { |
| NSArray *command = @[ @"codesign", @"-r-", @"--display", file]; |
| log_v("Probing signature of %s", file.fileSystemRepresentation); |
| int err = xcrunToolCommand(command, |
| ^(NSData *stdOutData, NSData *stdErrData, int err) { |
| if (!err) { d = [stdOutData retain]; } |
| }); |
| if (err) { return 0; } |
| } |
| return [d autorelease]; |
| } |
| |
| int main(int argc, const char *argv[]) |
| { |
| @autoreleasepool { |
| NSFileManager *fm = [NSFileManager defaultManager]; |
| |
| // Exxecutables to scan for Swift references. |
| // --scan-executable |
| NSMutableArray *executables = [NSMutableArray array]; |
| |
| // Directories to scan for more executables. |
| // --scan-folder |
| NSMutableArray *embedDirs = [NSMutableArray array]; |
| |
| // Platform name. |
| // --platform |
| // or the last path component of --source-libraries |
| NSString *platform = nil; |
| |
| // Copy source. |
| // --source-libraries |
| // or /path/to/swift-stdlib-tool/../../lib/swift/<--platform> |
| NSString *src_dir = nil; |
| |
| // Copy destinations, signed and unsigned. |
| // --destination and --unsigned-destination |
| NSString *dst_dir = nil; |
| NSString *unsigned_dst_dir = nil; |
| |
| // Resource copy destination. |
| // --resource-destination |
| NSString *resource_dst_dir = nil; |
| |
| // Resource libraries. |
| // --resource-library |
| NSMutableArray *resourceLibraries = [NSMutableArray new]; |
| |
| // Code signing options. |
| NSString *ident = nil; |
| NSString *keychain = nil; |
| NSMutableArray *otherCodesignFlags = [NSMutableArray array]; |
| |
| // Read arguments |
| bool print = false; |
| bool copy = false; |
| bool stripBitcode = false; |
| for (int i = 1; i < argc; i++) { |
| if (0 == strcmp(argv[i], "--print")) print = true; |
| if (0 == strcmp(argv[i], "--copy")) copy = true; |
| if (0 == strcmp(argv[i], "--verbose")) Verbose++; |
| if (0 == strcmp(argv[i], "--help")) { |
| printUsage(); |
| exit(0); |
| } |
| |
| if (0 == strcmp(argv[i], "--scan-executable")) { |
| [executables addObject:[NSString stringWithUTF8String:argv[++i]]]; |
| } |
| if (0 == strcmp(argv[i], "--scan-folder")) { |
| [embedDirs addObject:[NSString stringWithUTF8String:argv[++i]]]; |
| } |
| if (0 == strcmp(argv[i], "--source-libraries")) { |
| src_dir = [NSString stringWithUTF8String:argv[++i]]; |
| } |
| if (0 == strcmp(argv[i], "--platform")) { |
| platform = [NSString stringWithUTF8String:argv[++i]]; |
| } |
| |
| if (0 == strcmp(argv[i], "--destination")) { |
| dst_dir = [NSString stringWithUTF8String:argv[++i]]; |
| } |
| if (0 == strcmp(argv[i], "--unsigned-destination")) { |
| unsigned_dst_dir = [NSString stringWithUTF8String:argv[++i]]; |
| } |
| |
| if (0 == strcmp(argv[i], "--sign")) { |
| ident = [NSString stringWithUTF8String:argv[++i]]; |
| } |
| if (0 == strcmp(argv[i], "--keychain")) { |
| keychain = [NSString stringWithUTF8String:argv[++i]]; |
| } |
| if (0 == strcmp(argv[i], "--Xcodesign")) { |
| [otherCodesignFlags addObject: |
| [NSString stringWithUTF8String:argv[++i]]]; |
| } |
| |
| if (0 == strcmp(argv[i], "--strip-bitcode")) { |
| stripBitcode = true; |
| } |
| |
| if (0 == strcmp(argv[i], "--resource-destination")) { |
| resource_dst_dir = [NSString stringWithUTF8String:argv[++i]]; |
| } |
| if (0 == strcmp(argv[i], "--resource-library")) { |
| [resourceLibraries addObject:[NSString stringWithUTF8String:argv[++i]]]; |
| } |
| } |
| |
| // Fix up src_dir and platform values. |
| if (!src_dir && !platform) { |
| // Neither src_dir nor platform is set. Die. |
| fail_usage("At least one of --source-libraries and --platform " |
| "must be set."); |
| } |
| else if (!src_dir) { |
| // platform is set but src_dir is not. |
| // Use platform to set src_dir relative to us. |
| src_dir = [[[self_executable stringByDeletingLastPathComponent] |
| stringByDeletingLastPathComponent] |
| sst_stringByAppendingPathComponents: |
| @[ @"lib", @"swift", platform ]]; |
| } else if (!platform) { |
| // src_dir is set but platform is not. |
| // Pick platform from src_dir's name. |
| platform = src_dir.lastPathComponent; |
| } |
| |
| // Add the platform to unsigned_dst_dir if it is not already present. |
| if (unsigned_dst_dir) { |
| NSString *unsigned_platform = unsigned_dst_dir.lastPathComponent; |
| if (![platform isEqualToString:unsigned_platform]) { |
| unsigned_dst_dir = |
| [unsigned_dst_dir stringByAppendingPathComponent:platform]; |
| } |
| } |
| |
| // If the user specifies --strip-bitcode but not --sign, this |
| // will cause the dylibs to get copied, stripped, but not resigned. |
| // This will cause apps to fail to launch because the code signature |
| // is invalid. In this case, ignore --strip-bitcode. |
| if (stripBitcode && !ident) { |
| stripBitcode = false; |
| } |
| |
| // Collect executables from the --scan-folder locations. |
| for (NSString *embedDir in embedDirs) @autoreleasepool { |
| NSDirectoryEnumerator *dir = [fm enumeratorAtPath:embedDir]; |
| for (NSString *p in dir) @autoreleasepool { |
| BOOL isDir; |
| NSString *p2 = [embedDir stringByAppendingPathComponent:p]; |
| if ([fm fileExistsAtPath:p2 isDirectory:&isDir] && |
| !isDir && |
| [fm isExecutableFileAtPath:p2]) |
| { |
| [executables addObject:p2]; |
| } else { |
| log_vv("%s is not an executable file", |
| p2.fileSystemRepresentation); |
| } |
| } |
| } |
| |
| // Collect Swift library names from the input files. |
| // If the library does not exist in src_dir then assume the user wrote |
| // their own library named libswift* and is handling it elsewhere. |
| NSMutableDictionary *swiftLibs = [NSMutableDictionary new]; |
| for (NSString *path in executables) @autoreleasepool { |
| process(path, |
| ^(NSString *linkedLib) { |
| @autoreleasepool { |
| NSString *linkedSrc = |
| [src_dir stringByAppendingPathComponent:linkedLib]; |
| if ([fm fileExistsAtPath:linkedSrc]) { |
| swiftLibs[linkedLib] = [NSMutableSet set]; |
| } |
| } |
| }, |
| nil); |
| } |
| |
| // Collect more Swift library names from the Swift libraries themselves. |
| // Also collect the Swift libraries' UUIDs. |
| NSMutableArray *worklist = [swiftLibs.allKeys mutableCopy]; |
| while (worklist.count) @autoreleasepool { |
| NSString *lib = [worklist lastObject]; |
| [worklist removeLastObject]; |
| NSString *path = [src_dir stringByAppendingPathComponent:lib]; |
| process(path, |
| ^(NSString *linkedLib) { |
| @autoreleasepool { |
| NSString *linkedSrc = |
| [src_dir stringByAppendingPathComponent:linkedLib]; |
| if (!swiftLibs[linkedLib] && |
| [fm fileExistsAtPath:linkedSrc]) |
| { |
| swiftLibs[linkedLib] = [NSMutableSet set]; |
| [worklist addObject:linkedLib]; |
| } |
| } |
| }, |
| ^(NSUUID *uuid) { |
| NSMutableSet *uuids = swiftLibs[lib]; |
| [uuids addObject:uuid]; |
| }); |
| } |
| [worklist release]; |
| |
| // Collect all the Swift libraries that the user requested |
| // with --resource-library. |
| NSMutableDictionary *swiftLibsForResources = [NSMutableDictionary new]; |
| for (NSString *lib in resourceLibraries) @autoreleasepool { |
| NSString *libSrc = [src_dir stringByAppendingPathComponent:lib]; |
| if ([fm fileExistsAtPath:libSrc]) { |
| swiftLibsForResources[lib] = [NSMutableSet set]; |
| } |
| } |
| |
| // Collect dependencies of --resource-library libs. |
| worklist = [swiftLibsForResources.allKeys mutableCopy]; |
| while (worklist.count) @autoreleasepool { |
| NSString *lib = [worklist lastObject]; |
| [worklist removeLastObject]; |
| NSString *path = [src_dir stringByAppendingPathComponent:lib]; |
| process(path, |
| ^(NSString *linkedLib) { |
| NSString *linkedSrc = |
| [src_dir stringByAppendingPathComponent:linkedLib]; |
| if (!swiftLibsForResources[linkedLib] && |
| [fm fileExistsAtPath:linkedSrc]) |
| { |
| swiftLibsForResources[linkedLib] = [NSMutableSet set]; |
| [worklist addObject:linkedLib]; |
| } |
| }, |
| ^(NSUUID *uuid) { |
| NSMutableSet *uuids = swiftLibsForResources[lib]; |
| [uuids addObject:uuid]; |
| }); |
| } |
| [worklist release]; |
| |
| // Print the Swift libraries (full path to toolchain's copy) |
| if (print) { |
| for (NSString *lib in swiftLibs) { |
| printf("%s\n", [[src_dir stringByAppendingPathComponent:lib] |
| fileSystemRepresentation]); |
| } |
| } |
| |
| // Copy the Swift libraries to $build_dir/$frameworks |
| // and $build_dir/$unsigned_frameworks |
| if (copy) { |
| copyLibraries(src_dir, dst_dir, swiftLibs, stripBitcode); |
| if (unsigned_dst_dir) { |
| // Never strip bitcode from the unsigned libraries. |
| // Their existing signatures must be preserved. |
| copyLibraries(src_dir, unsigned_dst_dir, swiftLibs, false); |
| } |
| |
| if (resource_dst_dir) { |
| // Never strip bitcode from resources libraries, for |
| // the same reason as the libraries copied to |
| // unsigned_dst_dir. |
| copyLibraries(src_dir, resource_dst_dir, swiftLibsForResources, false); |
| } |
| } |
| |
| // Codesign the Swift libraries in $build_dir/$frameworks |
| // but not the libraries in $build_dir/$unsigned_frameworks. |
| if (ident) { |
| // Swift libraries that are up-to-date get codesigned anyway |
| // (in case options changed or a previous build was incomplete). |
| // We do employ an optimization, however, if resigning the dylib |
| // results in getting the same signing identity and credentials |
| // then we keep the original file to optimize for delta updates |
| // to the device. |
| |
| __block bool signedOne = false; |
| NSLock *signingLock = [NSLock new]; |
| |
| [swiftLibs enumerateKeysAndObjectsWithOptions: NSEnumerationConcurrent usingBlock: ^(id key, id value, BOOL *stop) { |
| |
| // Work around authentication UI problems |
| // by signing one synchronously and then signing the rest. |
| [signingLock lock]; |
| if (signedOne) { |
| // First signer is complete. Proceed concurrently. |
| [signingLock unlock]; |
| } else { |
| // We are the first signer. Hold the lock until we finish. |
| } |
| |
| NSString *lib = key; |
| NSString *dst = [dst_dir stringByAppendingPathComponent:lib]; |
| |
| // Get the code signature, and copy the dylib to the side |
| // to preserve it in case it does not change. We can use |
| // this to avoid unnecessary copies during delta installs |
| // to devices. |
| NSData *oldSignatureData = query_code_signature(dst); |
| const char *tmpFilePath = 0; |
| if (oldSignatureData) { |
| // Make a copy of the existing file, with permissions and |
| // mtime preserved. |
| NSString *tmpFile = [dst stringByAppendingPathExtension:@"original"]; |
| tmpFilePath = tmpFile.fileSystemRepresentation; |
| xcrunToolCommand(@[@"cp", @"-p", dst, tmpFile]); |
| } |
| |
| // Proceed with (re-)codesigning. |
| log_v("Codesigning %s at %s", lib.fileSystemRepresentation, dst_dir.fileSystemRepresentation); |
| |
| // Build the codesign invocation. |
| NSMutableArray *commandAndArguments = |
| [NSMutableArray arrayWithObjects: |
| @"codesign", |
| @"--force", @"--sign", ident, @"--verbose", nil]; |
| |
| if (keychain) { |
| [commandAndArguments addObject:@"--keychain"]; |
| [commandAndArguments addObject:keychain]; |
| } |
| |
| // Other codesign flags come later |
| // so they can override the default flags. |
| [commandAndArguments addObjectsFromArray:otherCodesignFlags]; |
| |
| [commandAndArguments addObject:dst]; |
| |
| int err = xcrunToolCommand(commandAndArguments); |
| |
| // Fail if codesign failed. |
| if (err) { |
| // Clean up any temporary files. |
| if (tmpFilePath) |
| unlink(tmpFilePath); |
| fail("Couldn't codesign %s: codesign failed with " |
| "exit code %d", dst.fileSystemRepresentation, err); |
| } |
| |
| // If we have an existing code signature data, query the new one and compare |
| // it with the code signature of the file before we re-signed it. |
| // If they are the same, use the original file instead. This preserves |
| // the contents of the file and mtime for use with delta installs. |
| if (oldSignatureData) { |
| NSData *newSignatureData = query_code_signature(dst); |
| |
| #if 0 |
| // For Debugging. |
| fprintf(stdout, "oldSignature (%lu bytes)\n", (unsigned long)oldSignatureData.length); |
| fwrite(oldSignatureData.bytes, oldSignatureData.length, 1, stdout); |
| fprintf(stdout, "\nnewSignature (%lu bytes)\n", (unsigned long)newSignatureData.length); |
| fwrite(newSignatureData.bytes, newSignatureData.length, 1, stdout); |
| fprintf(stdout, "\n"); |
| fflush(stdout); |
| #endif |
| |
| unsigned newLength = newSignatureData.length; |
| if (newLength == oldSignatureData.length && |
| memcmp(newSignatureData.bytes, oldSignatureData.bytes, newLength) == 0) { |
| log_v("Code signature of %s is unchanged; keeping original", lib.fileSystemRepresentation); |
| // The two signatures match. Unlink the new file, and re-link the old file. |
| const char *filePath = dst.fileSystemRepresentation; |
| unlink(filePath); |
| link(tmpFilePath, filePath); |
| } |
| } |
| // Clean up any temporary files. |
| if (tmpFilePath) { |
| unlink(tmpFilePath); |
| } |
| |
| if (!signedOne) { |
| // We are the first signer. Allow the others to proceed now. |
| signedOne = true; |
| [signingLock unlock]; |
| } |
| }]; |
| [signingLock release]; |
| } |
| } |
| exit(0); |
| } |