IJPL-172978 +docs

(cherry picked from commit d6238e51184e5161690a182a4b94af5a18be1307)

IJ-CR-151551

GitOrigin-RevId: 05377185001ef9d26691d2c7f71a273dd271b969
This commit is contained in:
Dmitriy.Panov
2024-12-17 15:35:43 +01:00
committed by intellij-monorepo-bot
parent 9f38e13c1b
commit 85e522304a
2 changed files with 106 additions and 33 deletions

View File

@@ -168,7 +168,15 @@ open class MacDistributionCustomizer {
}
/**
* @see org.jetbrains.intellij.build.NativeBinaryDownloader.getLauncher
* Generates a UUID to be used as an identity of an IDE Mach-O image.
*
* For a native macOS app, the Apple linker (ld) sets the build UUID based on a hash of the built code. But for an IDE it doesn't work that way.
* For different IDEs, the source code can be different, but a [org.jetbrains.intellij.build.NativeBinaryDownloader.getLauncher] binary may be exactly the same.
* So, the different IDEs may get the same UUIDs.
* And according to [the technote](https://developer.apple.com/documentation/technotes/tn3178-checking-for-and-resolving-build-uuid-problems), this may lead to the troubles:
* > Each distinct Mach-O image must have its own unique build UUID.
* > If you have two apps with different bundle IDs and the same main executable UUID, you might encounter weird problems with those subsystems.
* > For example, the network subsystem might apply constraints for one of your apps to the other app.
*/
@ApiStatus.Internal
open fun getDistributionUUID(context: BuildContext, currentUuid: UUID?): UUID {

View File

@@ -8,6 +8,7 @@ import org.jetbrains.intellij.build.MacDistributionCustomizer
import org.jetbrains.intellij.build.io.runProcess
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.channels.SeekableByteChannel
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
@@ -20,53 +21,97 @@ import java.util.*
*/
@ApiStatus.Internal
class MachOUuid(private val executable: Path, private val customizer: MacDistributionCustomizer, private val context: BuildContext) {
/**
* The 64-bit mach header appears at the very beginning of object files for 64-bit architectures:
* ```
* struct mach_header_64 {
* uint32_t magic; /* mach magic number identifier */
* cpu_type_t cputype; /* cpu specifier */
* cpu_subtype_t cpusubtype; /* machine specifier */
* uint32_t filetype; /* type of file */
* uint32_t ncmds; /* number of load commands */
* uint32_t sizeofcmds; /* the size of all the load commands */
* uint32_t flags; /* flags */
* uint32_t reserved; /* reserved */
* };
* ```
*
* The load commands directly follow the mach_header:
* ```
* struct load_command {
* uint32_t cmd; /* type of load command */
* uint32_t cmdsize; /* total size of command in bytes */
* };
* ```
*
* See https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/loader.h
*/
private companion object {
const val LC_UUID = 0x1b
const val LC_UUID: Int = 0x1b
/**
* [Integer.toHexString] is "feedfacf"
*/
const val MH_MAGIC_64: Int = -0x1120531
/**
* `mach_header_64.magic` size is four bytes (`uint32_t`)
*/
const val MACH_HEADER_64_MAGIC_SIZE_IN_BYTES: Int = 4
/**
* The start position of `mach_header_64.ncmds`
*/
const val MACH_HEADER_64_NCMDS_POSITION_IN_BYTES: Long = 16
/**
* `load_command.cmd`(`uint32_t`) + `load_command.cmdsize`(`uint32_t`)
*/
const val LOAD_COMMAND_HEADER_SIZE_IN_BYTES: Int = 8
/**
* The start position of `load_command`s, right after the `mach_header_64`
*/
const val LOAD_COMMANDS_POSITION_IN_BYTES: Long = 32
}
private val canBeSignedLocally: Boolean = context.options.isInDevelopmentMode && SystemInfoRt.isMac
private fun checkMagic(channel: SeekableByteChannel, buffer: ByteBuffer) {
channel.read(buffer)
buffer.flip()
val magic = buffer.getInt()
check(magic == MH_MAGIC_64) { "Not a valid 64-bit Mach-O file: 0x" + Integer.toHexString(magic) }
buffer.clear()
}
private fun readNumberOfLoadCommands(channel: SeekableByteChannel, buffer: ByteBuffer): Int {
channel.position(MACH_HEADER_64_NCMDS_POSITION_IN_BYTES)
channel.read(buffer)
buffer.flip()
return buffer.getInt()
}
suspend fun patch() {
val canBeSignedLocally = context.options.isInDevelopmentMode && SystemInfoRt.isMac
Files.newByteChannel(executable, StandardOpenOption.READ, StandardOpenOption.WRITE).use { channel ->
var buffer = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN)
channel.read(buffer)
buffer.flip()
val magic = buffer.getInt()
check(magic == -0x1120531) { "Not a valid 64-bit Mach-O file: 0x" + Integer.toHexString(magic) }
buffer.clear()
channel.position(16)
channel.read(buffer)
buffer.flip()
val nCmds = buffer.getInt()
buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN)
channel.position(32)
var buffer = ByteBuffer.allocate(MACH_HEADER_64_MAGIC_SIZE_IN_BYTES).order(ByteOrder.LITTLE_ENDIAN)
checkMagic(channel, buffer)
val nCmds = readNumberOfLoadCommands(channel, buffer)
buffer = ByteBuffer.allocate(LOAD_COMMAND_HEADER_SIZE_IN_BYTES).order(ByteOrder.LITTLE_ENDIAN)
channel.position(LOAD_COMMANDS_POSITION_IN_BYTES)
(0..<nCmds).forEach {
buffer.clear()
channel.read(buffer)
buffer.flip()
val cmd = buffer.getInt()
val cmdSize = buffer.getInt()
val cmdBodySize = cmdSize - LOAD_COMMAND_HEADER_SIZE_IN_BYTES
if (cmd == LC_UUID) {
buffer = ByteBuffer.allocate(16).order(ByteOrder.BIG_ENDIAN)
channel.read(buffer)
buffer.flip()
val msb = buffer.getLong()
val lsb = buffer.getLong()
val currentUuid = UUID(msb, lsb)
context.messages.info("current UUID of $executable: $currentUuid")
buffer.clear()
val newUuid = customizer.getDistributionUUID(context, currentUuid)
buffer.putLong(newUuid.mostSignificantBits)
buffer.putLong(newUuid.leastSignificantBits)
buffer.flip()
if (context.isMacCodeSignEnabled || canBeSignedLocally) {
channel.position(channel.position() - 16)
channel.write(buffer)
context.messages.info("new UUID of $executable: $newUuid")
}
patchUUID(channel, cmdBodySize)
return@use
}
else {
channel.position(channel.position() + cmdSize - 8)
channel.position(channel.position() + cmdBodySize)
}
}
context.messages.error("LC_UUID not found in $executable")
@@ -76,4 +121,24 @@ class MachOUuid(private val executable: Path, private val customizer: MacDistrib
runProcess(listOf("codesign", "--sign", "-", "--force", executable.toString()), inheritOut = true)
}
}
private fun patchUUID(channel: SeekableByteChannel, cmdBodySize: Int) {
val buffer = ByteBuffer.allocate(cmdBodySize).order(ByteOrder.BIG_ENDIAN)
channel.read(buffer)
buffer.flip()
val mostSigBits = buffer.getLong()
val leastSigBits = buffer.getLong()
val currentUuid = UUID(mostSigBits, leastSigBits)
context.messages.info("current UUID of $executable: $currentUuid")
buffer.clear()
val newUuid = customizer.getDistributionUUID(context, currentUuid)
buffer.putLong(newUuid.mostSignificantBits)
buffer.putLong(newUuid.leastSignificantBits)
buffer.flip()
if (context.isMacCodeSignEnabled || canBeSignedLocally) {
channel.position(channel.position() - cmdBodySize)
channel.write(buffer)
context.messages.info("new UUID of $executable: $newUuid")
}
}
}