Remove block dependency from the tree (#65942)

Closes https://github.com/zed-industries/zed/issues/22408

See also
https://github.com/zed-industries/scap/commit/b5c86ec58dd4b6b7732bf6ac8be03d6b1f190a82
that makes all macOS dependencies optional: we do not use these in Zed
anyway, and have `default-features = false` that allow to remove all
transitive `block` dependencies from the tree.

Adjust the test to ensure no such dependencies can appear later.

Release Notes:

- [GPUI] Removed `block` dependecy from the tree
This commit is contained in:
Kirill Bulatov authored and GitHub committed 2026-10-26 11:20:06 +00:00
1 parent 2a74c8dff6
commit 1808df02c0
12 files changed
+1484 -1004

No files matched your search

Generated
+7 -186
View File
@@ -2304,12 +2304,6 @@ dependencies = [
"core2",
]
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -3198,36 +3192,6 @@ dependencies = [
"thiserror 2.0.17",
]
[[package]]
name = "cocoa"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
dependencies = [
"bitflags 1.3.2",
"block",
"cocoa-foundation",
"core-foundation 0.9.4",
"core-graphics 0.23.2",
"foreign-types 0.5.0",
"libc",
"objc",
]
[[package]]
name = "cocoa-foundation"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
dependencies = [
"bitflags 1.3.2",
"block",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
"libc",
"objc",
]
[[package]]
name = "codespan-reporting"
version = "0.13.0"
@@ -3788,19 +3752,6 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core-graphics"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
"foreign-types 0.5.0",
"libc",
]
[[package]]
name = "core-graphics"
version = "0.24.0"
@@ -3809,35 +3760,11 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.13.1",
"core-foundation 0.10.0",
"core-graphics-types 0.2.0",
"core-graphics-types",
"foreign-types 0.5.0",
"libc",
]
[[package]]
name = "core-graphics-helmer-fork"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5"
dependencies = [
"bitflags 2.13.1",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
"foreign-types 0.5.0",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.2.0"
@@ -3865,7 +3792,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130"
dependencies = [
"core-foundation 0.10.0",
"core-graphics 0.24.0",
"core-graphics",
"foreign-types 0.5.0",
"libc",
]
@@ -7509,12 +7436,10 @@ dependencies = [
"collections",
"core-foundation 0.10.0",
"core-foundation-sys",
"core-graphics 0.24.0",
"core-graphics",
"core-text",
"derive_more",
"dispatch2",
"etagere",
"foreign-types 0.5.0",
"futures 0.3.34",
"gpui",
"gpui_util",
@@ -7522,9 +7447,9 @@ dependencies = [
"libc",
"log",
"mach2",
"metal",
"objc2 0.6.3",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-core-text",
"objc2-core-video",
"objc2-foundation 0.3.2",
@@ -7614,7 +7539,7 @@ dependencies = [
"collections",
"core-foundation 0.10.0",
"core-foundation-sys",
"core-graphics 0.24.0",
"core-graphics",
"ctor",
"dispatch2",
"foreign-types 0.5.0",
@@ -10583,15 +10508,6 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc04a4c58212d57930a24bf47d3fa87485264a3a054e9c10e042eb373573ad3c"
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]]
name = "manatee"
version = "0.8.0-alpha.5"
@@ -10903,21 +10819,6 @@ dependencies = [
"unicode-width 0.2.2",
]
[[package]]
name = "metal"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15"
dependencies = [
"bitflags 2.13.1",
"block",
"core-graphics-types 0.2.0",
"foreign-types 0.5.0",
"log",
"objc",
"paste",
]
[[package]]
name = "micromap"
version = "0.3.0"
@@ -11686,27 +11587,6 @@ dependencies = [
"url",
]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
"objc_exception",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]]
name = "objc-sys"
version = "0.3.5"
@@ -12063,24 +11943,6 @@ dependencies = [
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc_exception"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4"
dependencies = [
"cc",
]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]]
name = "object"
version = "0.37.3"
@@ -15942,29 +15804,6 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
name = "scratch"
version = "1.0.9"
[[package]]
name = "screencapturekit"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e"
dependencies = [
"screencapturekit-sys",
]
[[package]]
name = "screencapturekit-sys"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60"
dependencies = [
"block",
"dispatch",
"objc",
"objc-foundation",
"objc_id",
"once_cell",
]
[[package]]
name = "scroll"
version = "0.12.0"
@@ -17948,18 +17787,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "tao-core-video-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6"
dependencies = [
"cfg-if",
"core-foundation-sys",
"libc",
"objc",
]
[[package]]
name = "target-lexicon"
version = "0.13.5"
@@ -22716,7 +22543,7 @@ dependencies = [
"bitflags 2.13.1",
"byteorder",
"core-foundation 0.10.0",
"core-graphics 0.24.0",
"core-graphics",
"core-text",
"dirs",
"dwrote",
@@ -22784,18 +22611,12 @@ dependencies = [
[[package]]
name = "zed-scap"
version = "0.0.8-zed"
source = "git+https://github.com/zed-industries/scap?rev=4afea48c3b002197176fb19cd0f9b180dd36eaac#4afea48c3b002197176fb19cd0f9b180dd36eaac"
source = "git+https://github.com/zed-industries/scap?rev=b5c86ec58dd4b6b7732bf6ac8be03d6b1f190a82#b5c86ec58dd4b6b7732bf6ac8be03d6b1f190a82"
dependencies = [
"anyhow",
"cocoa",
"core-graphics-helmer-fork",
"log",
"objc",
"rand 0.8.6",
"screencapturekit",
"screencapturekit-sys",
"sysinfo 0.31.4",
"tao-core-video-sys",
"windows 0.61.3",
"windows-capture",
"x11",
+1 -2
View File
@@ -692,7 +692,6 @@ mach2 = "0.6"
markup5ever_rcdom = "0.39"
mdbook = { version = "=0.4.40", default-features = false, features = ["search"] }
merman = { version = "=0.8.0-alpha.5", default-features = false }
metal = "0.33"
mime = "0.3.17"
minidumper = "0.11"
moka = { version = "0.12.10", features = ["sync"] }
@@ -885,7 +884,7 @@ rustix = { version = "1.1", features = ["fs"] }
rustls = "0.23.26"
rustls-platform-verifier = "0.7"
# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
scap = { git = "https://github.com/zed-industries/scap", rev = "b5c86ec58dd4b6b7732bf6ac8be03d6b1f190a82", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
schemars = { version = "1.0", default-features = false, features = ["std", "indexmap2"] }
sea-orm = { version = "=1.1.10", default-features = false }
seccompiler = "0.5"
+25 -7
View File
@@ -31,10 +31,8 @@ core-foundation.workspace = true
core-foundation-sys.workspace = true
core-graphics.workspace = true
core-text = { workspace = true, optional = true }
derive_more.workspace = true
dispatch2.workspace = true
dispatch2 = { workspace = true, features = ["block2"] }
etagere.workspace = true
foreign-types.workspace = true
futures.workspace = true
# WARNING: If you change this, you must also publish a new version of zed-font-kit to crates.io
font-kit = { workspace = true, optional = true, features = ["source"] }
@@ -43,14 +41,34 @@ image.workspace = true
libc.workspace = true
log.workspace = true
mach2.workspace = true
metal.workspace = true
objc2.workspace = true
objc2-core-foundation = { workspace = true, features = ["std", "CFData", "CFDictionary", "CFString", "CFNumber", "CFBase"] }
objc2-core-foundation = { workspace = true, features = ["std", "CFData", "CFDictionary", "CFString", "CFNumber", "CFBase", "CFCGTypes"] }
objc2-core-graphics = { workspace = true, features = ["CGColorSpace"] }
objc2-core-text = { workspace = true, optional = true }
objc2-core-video = { workspace = true, features = ["CVPixelBuffer", "CVMetalTexture", "CVMetalTextureCache", "CVReturn", "objc2", "objc2-metal"] }
objc2-metal = { version = "0.3", default-features = false, features = ["MTLDevice", "MTLPixelFormat", "MTLTexture"] }
objc2-metal = { version = "0.3.2", default-features = false, features = [
"std",
"block2",
"dispatch2",
"MTLAllocation",
"MTLBlitCommandEncoder",
"MTLBuffer",
"MTLCommandBuffer",
"MTLCommandEncoder",
"MTLCommandQueue",
"MTLDevice",
"MTLDrawable",
"MTLLibrary",
"MTLPixelFormat",
"MTLRenderCommandEncoder",
"MTLRenderPass",
"MTLRenderPipeline",
"MTLResource",
"MTLTexture",
"MTLTypes",
] }
objc2-foundation = { workspace = true, features = ["NSThread", "NSNotification", "NSOperation", "NSProcessInfo", "block2"] }
objc2-quartz-core.workspace = true
objc2-quartz-core = { workspace = true, features = ["objc2-core-foundation", "objc2-core-graphics", "objc2-metal"] }
objc2-security.workspace = true
parking_lot.workspace = true
pathfinder_geometry = { workspace = true, optional = true }
+259 -92
View File
@@ -1,23 +1,29 @@
use anyhow::{Context as _, Result};
use derive_more::{Deref, DerefMut};
use anyhow::{Context as _, Result, ensure};
use etagere::BucketedAtlasAllocator;
use gpui::{
AtlasBackend, AtlasKey, AtlasState, AtlasTextureId, AtlasTextureKind, AtlasTextureList,
AtlasTile, Bounds, DevicePixels, PlatformAtlas, Point, Size,
};
use metal::Device;
use objc2::{rc::Retained, runtime::ProtocolObject};
use objc2_metal::{
MTLDevice, MTLOrigin, MTLPixelFormat, MTLRegion, MTLSize, MTLStorageMode, MTLTexture,
MTLTextureDescriptor, MTLTextureUsage,
};
use parking_lot::Mutex;
use std::borrow::Cow;
use std::{borrow::Cow, ptr::NonNull};
pub struct MetalAtlas(Mutex<AtlasState<MetalAtlasTextures>>);
impl MetalAtlas {
pub(crate) fn new(device: Device, supports_shared_storage: bool) -> Self {
pub(crate) fn new(
device: Retained<ProtocolObject<dyn MTLDevice>>,
supports_shared_storage: bool,
) -> Self {
MetalAtlas(Mutex::new(AtlasState::new(MetalAtlasTextures {
device: AssertSend(device),
device,
supports_shared_storage,
monochrome_textures: Default::default(),
polychrome_textures: Default::default(),
monochrome_textures: AtlasTextureList::default(),
polychrome_textures: AtlasTextureList::default(),
})))
}
@@ -25,13 +31,16 @@ impl MetalAtlas {
/// has been removed. A scene can still reference such a texture when a
/// cached view replays a paint from before the image was dropped, so
/// callers must skip those sprites rather than assume the texture exists.
pub(crate) fn metal_texture(&self, id: AtlasTextureId) -> Option<metal::Texture> {
pub(crate) fn metal_texture(
&self,
id: AtlasTextureId,
) -> Option<Retained<ProtocolObject<dyn MTLTexture>>> {
Some(self.0.lock().backend.texture(id)?.metal_texture.clone())
}
}
struct MetalAtlasTextures {
device: AssertSend<Device>,
device: Retained<ProtocolObject<dyn MTLDevice>>,
supports_shared_storage: bool,
monochrome_textures: AtlasTextureList<MetalAtlasTexture>,
polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
@@ -58,11 +67,12 @@ impl AtlasBackend for MetalAtlasTextures {
size: Size<DevicePixels>,
bytes: &[u8],
) -> Result<AtlasTile> {
let tile = self.allocate(size, kind).context("failed to allocate")?;
let upload = AtlasUpload::new(kind, size, bytes)?;
let tile = self.allocate(size, kind)?;
let texture = self
.texture(tile.texture_id)
.context("allocated tile refers to a missing texture")?;
texture.upload(tile.bounds, bytes);
.context("looking up allocated atlas texture")?;
texture.upload(tile.bounds.origin, &upload)?;
Ok(tile)
}
@@ -100,7 +110,7 @@ impl MetalAtlasTextures {
&mut self,
size: Size<DevicePixels>,
texture_kind: AtlasTextureKind,
) -> Option<AtlasTile> {
) -> Result<AtlasTile> {
{
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
@@ -113,19 +123,19 @@ impl MetalAtlasTextures {
.rev()
.find_map(|texture| texture.allocate(size))
{
return Some(tile);
return Ok(tile);
}
}
let texture = self.push_texture(size, texture_kind);
texture.allocate(size)
let texture = self.push_texture(size, texture_kind)?;
texture.allocate(size).context("allocating atlas tile")
}
fn push_texture(
&mut self,
min_size: Size<DevicePixels>,
kind: AtlasTextureKind,
) -> &mut MetalAtlasTexture {
) -> Result<&mut MetalAtlasTexture> {
const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
width: DevicePixels(1024),
height: DevicePixels(1024),
@@ -136,32 +146,38 @@ impl MetalAtlasTextures {
height: DevicePixels(16384),
};
let size = min_size.min(&MAX_ATLAS_SIZE).max(&DEFAULT_ATLAS_SIZE);
let texture_descriptor = metal::TextureDescriptor::new();
texture_descriptor.set_width(size.width.into());
texture_descriptor.set_height(size.height.into());
let texture_descriptor = MTLTextureDescriptor::new();
// SAFETY: Both dimensions are clamped to the existing 1024..=16384 atlas size limits.
unsafe {
texture_descriptor.setWidth(size.width.0 as usize);
texture_descriptor.setHeight(size.height.0 as usize);
}
let pixel_format;
let usage;
match kind {
AtlasTextureKind::Monochrome => {
pixel_format = metal::MTLPixelFormat::A8Unorm;
usage = metal::MTLTextureUsage::ShaderRead;
pixel_format = MTLPixelFormat::A8Unorm;
usage = MTLTextureUsage::ShaderRead;
}
AtlasTextureKind::Polychrome => {
pixel_format = metal::MTLPixelFormat::BGRA8Unorm;
usage = metal::MTLTextureUsage::ShaderRead;
pixel_format = MTLPixelFormat::BGRA8Unorm;
usage = MTLTextureUsage::ShaderRead;
}
AtlasTextureKind::Subpixel => unreachable!(),
}
texture_descriptor.set_pixel_format(pixel_format);
texture_descriptor.set_usage(usage);
texture_descriptor.setPixelFormat(pixel_format);
texture_descriptor.setUsage(usage);
// Shared memory mode can be used only on Apple GPU families
// https://developer.apple.com/documentation/metal/mtlresourceoptions/storagemodeshared
texture_descriptor.set_storage_mode(if self.supports_shared_storage {
metal::MTLStorageMode::Shared
texture_descriptor.setStorageMode(if self.supports_shared_storage {
MTLStorageMode::Shared
} else {
metal::MTLStorageMode::Managed
MTLStorageMode::Managed
});
let metal_texture = self.device.new_texture(&texture_descriptor);
let metal_texture = self
.device
.newTextureWithDescriptor(&texture_descriptor)
.context("allocating atlas texture")?;
let texture_list = match kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
@@ -169,28 +185,23 @@ impl MetalAtlasTextures {
AtlasTextureKind::Subpixel => unreachable!(),
};
let index = texture_list.free_list.pop();
let index = texture_list.free_list.pop().unwrap_or_else(|| {
let index = texture_list.textures.len();
texture_list.textures.push(None);
index
});
let atlas_texture = MetalAtlasTexture {
id: AtlasTextureId {
index: index.unwrap_or(texture_list.textures.len()) as u32,
index: index as u32,
kind,
},
allocator: etagere::BucketedAtlasAllocator::new(size_to_etagere(size)),
metal_texture: AssertSend(metal_texture),
metal_texture,
live_atlas_keys: 0,
};
if let Some(ix) = index {
texture_list.textures[ix] = Some(atlas_texture);
texture_list.textures.get_mut(ix)
} else {
texture_list.textures.push(Some(atlas_texture));
texture_list.textures.last_mut()
}
.unwrap()
.as_mut()
.unwrap()
Ok(texture_list.textures[index].insert(atlas_texture))
}
fn texture(&self, id: AtlasTextureId) -> Option<&MetalAtlasTexture> {
@@ -206,7 +217,7 @@ impl MetalAtlasTextures {
struct MetalAtlasTexture {
id: AtlasTextureId,
allocator: BucketedAtlasAllocator,
metal_texture: AssertSend<metal::Texture>,
metal_texture: Retained<ProtocolObject<dyn MTLTexture>>,
live_atlas_keys: u32,
}
@@ -226,28 +237,46 @@ impl MetalAtlasTexture {
Some(tile)
}
fn upload(&self, bounds: Bounds<DevicePixels>, bytes: &[u8]) {
let region = metal::MTLRegion::new_2d(
bounds.origin.x.into(),
bounds.origin.y.into(),
bounds.size.width.into(),
bounds.size.height.into(),
fn upload(&self, origin: Point<DevicePixels>, upload: &AtlasUpload<'_>) -> Result<()> {
ensure!(
self.id.kind == upload.kind,
"atlas upload kind does not match texture"
);
self.metal_texture.replace_region(
region,
0,
bytes.as_ptr() as *const _,
bounds.size.width.to_bytes(self.bytes_per_pixel()) as u64,
ensure!(
origin.x.0 >= 0
&& origin.y.0 >= 0
&& (origin.x.0 as usize)
.checked_add(upload.size.width.0 as usize)
.is_some_and(|end| end <= self.metal_texture.width())
&& (origin.y.0 as usize)
.checked_add(upload.size.height.0 as usize)
.is_some_and(|end| end <= self.metal_texture.height()),
"atlas upload is outside texture bounds"
);
}
fn bytes_per_pixel(&self) -> u8 {
use metal::MTLPixelFormat::*;
match self.metal_texture.pixel_format() {
A8Unorm | R8Unorm => 1,
RGBA8Unorm | BGRA8Unorm => 4,
_ => unimplemented!(),
let region = MTLRegion {
origin: MTLOrigin {
x: origin.x.0 as usize,
y: origin.y.0 as usize,
z: 0,
},
size: MTLSize {
width: upload.size.width.0 as usize,
height: upload.size.height.0 as usize,
depth: 1,
},
};
// SAFETY: Validated bytes cover every row of the checked region in this CPU-accessible texture.
// The borrowed bytes remain live for the copy; GPU ordering is a separate caller requirement.
unsafe {
self.metal_texture
.replaceRegion_mipmapLevel_withBytes_bytesPerRow(
region,
0,
NonNull::from(upload.bytes).cast(),
upload.bytes_per_row,
);
}
Ok(())
}
fn decrement_ref_count(&mut self) {
@@ -259,6 +288,44 @@ impl MetalAtlasTexture {
}
}
struct AtlasUpload<'a> {
kind: AtlasTextureKind,
size: Size<DevicePixels>,
bytes: &'a [u8],
bytes_per_row: usize,
}
impl<'a> AtlasUpload<'a> {
fn new(kind: AtlasTextureKind, size: Size<DevicePixels>, bytes: &'a [u8]) -> Result<Self> {
ensure!(
size.width.0 > 0 && size.height.0 > 0,
"atlas dimensions must be positive"
);
let bytes_per_pixel = match kind {
AtlasTextureKind::Monochrome => 1,
AtlasTextureKind::Polychrome => 4,
AtlasTextureKind::Subpixel => unreachable!(),
};
let bytes_per_row = (size.width.0 as usize)
.checked_mul(bytes_per_pixel)
.context("calculating atlas row byte count")?;
let byte_count = bytes_per_row
.checked_mul(size.height.0 as usize)
.context("calculating atlas byte count")?;
ensure!(
bytes.len() >= byte_count,
"atlas upload byte length is {}, expected at least {byte_count}",
bytes.len()
);
Ok(Self {
kind,
size,
bytes,
bytes_per_row,
})
}
}
fn size_to_etagere(size: Size<DevicePixels>) -> etagere::Size {
etagere::Size::new(size.width.into(), size.height.into())
}
@@ -270,39 +337,13 @@ fn point_from_etagere(value: etagere::Point) -> Point<DevicePixels> {
}
}
#[derive(Deref, DerefMut)]
struct AssertSend<T>(T);
unsafe impl<T> Send for AssertSend<T> {}
#[cfg(test)]
mod tests {
use super::*;
use gpui::PlatformAtlas;
use gpui::{PlatformAtlas, point, size};
use objc2_metal::{MTLCreateSystemDefaultDevice, MTLGPUFamily};
use std::borrow::Cow;
fn create_atlas() -> Option<MetalAtlas> {
let device = metal::Device::system_default()?;
Some(MetalAtlas::new(device, true))
}
fn make_image_key(image_id: usize, frame_index: usize) -> AtlasKey {
AtlasKey::Image(gpui::RenderImageParams {
image_id: gpui::ImageId(image_id),
frame_index,
})
}
fn insert_tile(atlas: &MetalAtlas, key: AtlasKey, size: Size<DevicePixels>) -> AtlasTile {
atlas
.get_or_insert_with(key, &mut || {
let byte_count = (size.width.0 as usize) * (size.height.0 as usize) * 4;
Ok(Some((size, Cow::Owned(vec![0u8; byte_count]))))
})
.expect("allocation should succeed")
.expect("callback returns Some")
}
#[test]
fn test_remove_clears_stale_keys_from_tiles_by_key() {
let Some(atlas) = create_atlas() else {
@@ -338,6 +379,7 @@ mod tests {
// Re-inserting A must allocate a fresh tile on a new texture,
// NOT return a stale tile referencing the deleted texture.
let tile_a2 = insert_tile(&atlas, key_a, small);
assert_eq!(tile_a2.texture_id, tile_a.texture_id);
// The texture must actually exist — this would panic before the fix.
assert!(atlas.metal_texture(tile_a2.texture_id).is_some());
@@ -402,4 +444,129 @@ mod tests {
let key = make_image_key(999, 0);
atlas.remove(&key);
}
#[test]
fn test_invalid_upload_does_not_allocate() {
let Some(atlas) = create_atlas() else {
return;
};
let mut state = atlas.0.lock();
for (kind, short_bytes, short_error) in [
(
AtlasTextureKind::Monochrome,
&[][..],
"atlas upload byte length is 0, expected at least 1",
),
(
AtlasTextureKind::Polychrome,
&[0][..],
"atlas upload byte length is 1, expected at least 4",
),
] {
for (width, height, bytes, expected_error) in [
(1, 1, short_bytes, short_error),
(0, 1, &[][..], "atlas dimensions must be positive"),
(1, 0, &[][..], "atlas dimensions must be positive"),
(i32::MIN, 1, &[][..], "atlas dimensions must be positive"),
(1, i32::MIN, &[][..], "atlas dimensions must be positive"),
] {
assert_eq!(
state
.backend
.insert(kind, size(DevicePixels(width), DevicePixels(height)), bytes)
.err()
.map(|error| error.to_string()),
Some(expected_error.to_owned())
);
for textures in [
&state.backend.monochrome_textures,
&state.backend.polychrome_textures,
] {
assert_eq!((textures.textures.len(), textures.free_list.len()), (0, 0));
}
}
}
}
#[test]
fn test_texture_upload_preserves_pixel_layout() -> Result<()> {
let Some(atlas) = create_atlas() else {
return Ok(());
};
let mut state = atlas.0.lock();
for (kind, bytes_per_pixel) in [
(AtlasTextureKind::Monochrome, 1),
(AtlasTextureKind::Polychrome, 4),
] {
let texture = state
.backend
.push_texture(size(DevicePixels(8), DevicePixels(8)), kind)?;
texture.upload(
Point::default(),
&AtlasUpload::new(
kind,
size(DevicePixels(8), DevicePixels(8)),
&vec![0; 8 * 8 * bytes_per_pixel],
)?,
)?;
let pixels = (1..=3 * 2 * bytes_per_pixel)
.map(|value| value as u8)
.collect::<Vec<_>>();
texture.upload(
point(DevicePixels(2), DevicePixels(3)),
&AtlasUpload::new(kind, size(DevicePixels(3), DevicePixels(2)), &pixels)?,
)?;
let mut actual = vec![0; 8 * 8 * bytes_per_pixel];
unsafe {
texture
.metal_texture
.getBytes_bytesPerRow_fromRegion_mipmapLevel(
NonNull::from(actual.as_mut_slice()).cast(),
8 * bytes_per_pixel,
MTLRegion {
origin: MTLOrigin { x: 0, y: 0, z: 0 },
size: MTLSize {
width: 8,
height: 8,
depth: 1,
},
},
0,
);
}
let mut expected = vec![0; actual.len()];
for row in 0..2 {
let start = ((row + 3) * 8 + 2) * bytes_per_pixel;
let source = row * 3 * bytes_per_pixel;
expected[start..start + 3 * bytes_per_pixel]
.copy_from_slice(&pixels[source..source + 3 * bytes_per_pixel]);
}
assert_eq!(actual, expected);
}
Ok(())
}
fn create_atlas() -> Option<MetalAtlas> {
let device = MTLCreateSystemDefaultDevice()?;
let supports_shared_storage =
cfg!(target_os = "ios") || device.supportsFamily(MTLGPUFamily::Apple1);
Some(MetalAtlas::new(device, supports_shared_storage))
}
fn make_image_key(image_id: usize, frame_index: usize) -> AtlasKey {
AtlasKey::Image(gpui::RenderImageParams {
image_id: gpui::ImageId(image_id),
frame_index,
})
}
fn insert_tile(atlas: &MetalAtlas, key: AtlasKey, size: Size<DevicePixels>) -> AtlasTile {
atlas
.get_or_insert_with(key, &mut || {
let byte_count = (size.width.0 as usize) * (size.height.0 as usize) * 4;
Ok(Some((size, Cow::Owned(vec![0u8; byte_count]))))
})
.expect("inserting atlas tile")
.expect("retrieving inserted atlas tile")
}
}
File diff suppressed because it is too large. Load diff
+10 -11
View File
@@ -326,6 +326,16 @@ impl IosWindow {
let layer = view.layer();
layer.setContentsScale(scale);
let mut renderer = MetalRenderer::from_layer(
MetalContext::default(),
layer
.downcast_ref::<CAMetalLayer>()
.ok_or_else(|| anyhow::anyhow!("UIKit view must have a CAMetalLayer"))?,
false,
)?;
let pixel_w = (screen_bounds_cg.size.width * scale) as i32;
let pixel_h = (screen_bounds_cg.size.height * scale) as i32;
renderer.update_drawable_size(size(DevicePixels(pixel_w), DevicePixels(pixel_h)));
// Auto-resize the Metal view when the parent view changes size
// (e.g. rotation). UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight
@@ -406,17 +416,6 @@ impl IosWindow {
text_input_view.setUserInteractionEnabled(true);
view.addSubview(&text_input_view);
let pixel_w = (screen_bounds_cg.size.width * scale) as i32;
let pixel_h = (screen_bounds_cg.size.height * scale) as i32;
let mut renderer = MetalRenderer::from_layer(
MetalContext::default(),
layer
.downcast_ref::<CAMetalLayer>()
.ok_or_else(|| anyhow::anyhow!("UIKit view must have a CAMetalLayer"))?,
false,
);
renderer.update_drawable_size(size(DevicePixels(pixel_w), DevicePixels(pixel_h)));
let state = IosWindowState {
platform: Rc::downgrade(platform),
keyboard_observers: RefCell::default(),
+1 -1
View File
@@ -770,7 +770,7 @@ impl Platform for MacPlatform {
background_executor,
renderer_context,
self.1,
)))
)?))
}
fn window_appearance(&self) -> WindowAppearance {
+4 -11
View File
@@ -4,7 +4,6 @@ use crate::{
kTISPropertyInputSourceIsASCIICapable, kTISPropertyInputSourceType, kTISTypeKeyboardInputMode,
ns_string, renderer,
};
#[cfg(any(test, feature = "test-support"))]
use anyhow::Result;
use block2::RcBlock;
use dispatch2::DispatchQueue;
@@ -959,8 +958,9 @@ impl MacWindow {
background_executor: BackgroundExecutor,
renderer_context: renderer::Context,
marker: MainThreadMarker,
) -> Self {
) -> Result<Self> {
autoreleasepool(|_| unsafe {
let renderer = renderer::new_renderer(renderer_context, false, use_display_p3)?;
let allows_automatic_window_tabbing = tabbing_identifier.is_some();
if allows_automatic_window_tabbing {
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: Bool::YES];
@@ -1072,14 +1072,7 @@ impl MacWindow {
cursor_style: CursorStyle::Arrow,
cursor_visible,
frame_source: None,
renderer: renderer::new_renderer(
renderer_context,
native_window as *mut _,
native_view as *mut _,
bounds.size.map(|pixels| pixels.as_f32()),
false,
use_display_p3,
),
renderer,
request_frame_callback: None,
event_callback: None,
activate_callback: None,
@@ -1285,7 +1278,7 @@ impl MacWindow {
window_state.sheet_parent = sheet_parent;
}
window
Ok(window)
})
}
+5
View File
@@ -26,6 +26,11 @@ runtime_shaders = ["gpui_macos/runtime_shaders", "gpui_ios/runtime_shaders"]
wayland = ["gpui_linux/wayland"]
x11 = ["gpui_linux/x11"]
[[bench]]
name = "atlas_upload"
harness = false
required-features = ["bench-support"]
[[bench]]
name = "focus_cleanup"
path = "../gpui/benches/focus_cleanup.rs"
@@ -0,0 +1,264 @@
use std::{borrow::Cow, hint::black_box, sync::Arc, time::Duration};
use anyhow::{Context as _, Result};
use bench_metrics::MetricReport;
use criterion::{BatchSize, BenchmarkId, Criterion, Throughput};
use gpui::{
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, BenchMeasurement, Bounds, ContentMask,
Corners, DevicePixels, Hsla, ImageId, MonochromeSprite, PaddedBool32, PlatformAtlas,
PlatformHeadlessRenderer, Point, PolychromeSprite, RenderImageParams, RenderSvgParams,
ScaledPixels, Scene, SharedString, Size, TransformationMatrix, size,
};
struct Entry {
key: AtlasKey,
bytes: Vec<u8>,
}
struct Fixture {
renderer: Box<dyn PlatformHeadlessRenderer>,
atlas: Arc<dyn PlatformAtlas>,
dimensions: Size<DevicePixels>,
seed: Entry,
entries: Vec<Entry>,
}
struct Batch<'a> {
fixture: &'a Fixture,
texture_id: AtlasTextureId,
misses: bool,
builder_calls: usize,
tiles: Vec<AtlasTile>,
}
fn atlas_upload(criterion: &mut Criterion<BenchMeasurement>) {
assert!(cfg!(target_os = "macos"), "this benchmark requires Metal");
assert!(!cfg!(debug_assertions), "use --profile release-fast");
let mut group = criterion.benchmark_group("atlas_upload");
for (kind, format) in [
(AtlasTextureKind::Monochrome, "a8"),
(AtlasTextureKind::Polychrome, "bgra8"),
] {
for (width, height, count) in [(1, 1, 128), (3, 2, 128), (16, 16, 128), (256, 256, 8)] {
let dimensions = size(DevicePixels(width), DevicePixels(height));
let mut fixture = Fixture::new(kind, dimensions, count).expect("create Metal fixture");
fixture.validate_pixels().expect("validate atlas pixels");
group.throughput(Throughput::Elements(count as u64));
for (misses, mode) in [(true, "miss"), (false, "hit")] {
let report = MetricReport::new();
let id = BenchmarkId::new(
format!("{mode}_{format}"),
format!("{width}x{height}_{count}"),
);
group.bench_function(id, |bencher| {
let mut iterations = 0;
report.discard_pending();
bencher.iter_batched_ref(
|| {
iterations += 1;
fixture.prepare(misses).expect("prepare atlas batch")
},
|batch| batch.run().expect("insert atlas batch"),
BatchSize::PerIteration,
);
report.record_sample(iterations * count as u64);
});
eprintln!("{mode}_{format}/{width}x{height}: secondary metrics per atlas lookup");
report.print(" ");
}
}
}
group.finish();
}
impl Fixture {
fn new(kind: AtlasTextureKind, dimensions: Size<DevicePixels>, count: usize) -> Result<Self> {
let renderer = gpui_platform::current_headless_renderer()?.context("Metal unavailable")?;
let atlas = renderer.sprite_atlas();
Ok(Self {
renderer,
atlas,
dimensions,
seed: make_entry(kind, dimensions, count),
entries: (0..count)
.map(|index| make_entry(kind, dimensions, index))
.collect(),
})
}
fn insert(&self, entry: &Entry, builder_calls: &mut usize) -> Result<AtlasTile> {
self.atlas
.get_or_insert_with(black_box(entry.key.clone()), &mut || {
*builder_calls += 1;
Ok(Some((
black_box(self.dimensions),
Cow::Borrowed(black_box(&entry.bytes)),
)))
})?
.context("missing atlas tile")
}
fn prepare(&self, misses: bool) -> Result<Batch<'_>> {
for entry in &self.entries {
self.atlas.remove(&entry.key);
}
self.atlas.remove(&self.seed.key);
let mut builder_calls = 0;
let seed = self.insert(&self.seed, &mut builder_calls)?;
assert_eq!(builder_calls, 1);
if !misses {
for entry in &self.entries {
let tile = self.insert(entry, &mut builder_calls)?;
assert_eq!(tile.texture_id, seed.texture_id);
}
assert_eq!(builder_calls, self.entries.len() + 1);
}
Ok(Batch {
fixture: self,
texture_id: seed.texture_id,
misses,
builder_calls: 0,
tiles: Vec::with_capacity(self.entries.len()),
})
}
fn validate_pixels(&mut self) -> Result<()> {
let tiles = {
let mut batch = self.prepare(true)?;
batch.run()?;
batch.tiles.clone()
};
for (entry, tile) in self.entries.iter().zip(tiles) {
let scene = tile_scene(tile);
let image = self
.renderer
.render_scene_to_image(&scene, self.dimensions)?;
let expected = match tile.texture_id.kind {
AtlasTextureKind::Monochrome => entry
.bytes
.iter()
.flat_map(|&alpha| [alpha, alpha, alpha, 255])
.collect::<Vec<_>>(),
AtlasTextureKind::Polychrome => entry
.bytes
.chunks_exact(4)
.flat_map(|pixel| [pixel[2], pixel[1], pixel[0], 255])
.collect(),
AtlasTextureKind::Subpixel => unreachable!(),
};
assert_eq!(image.as_raw(), &expected);
}
let mut batch = self.prepare(false)?;
batch.run()?;
Ok(())
}
}
impl Batch<'_> {
fn run(&mut self) -> Result<()> {
for entry in &self.fixture.entries {
self.tiles
.push(self.fixture.insert(entry, &mut self.builder_calls)?);
}
black_box(&self.tiles);
Ok(())
}
}
impl Drop for Batch<'_> {
fn drop(&mut self) {
if std::thread::panicking() {
return;
}
let count = self.fixture.entries.len();
assert_eq!(self.tiles.len(), count);
assert_eq!(self.builder_calls, if self.misses { count } else { 0 });
for tile in &self.tiles {
assert_eq!(tile.texture_id, self.texture_id);
assert_eq!(tile.bounds.size, self.fixture.dimensions);
}
}
}
fn make_entry(kind: AtlasTextureKind, dimensions: Size<DevicePixels>, index: usize) -> Entry {
let key = match kind {
AtlasTextureKind::Monochrome => AtlasKey::Svg(RenderSvgParams {
path: SharedString::from(format!("atlas-upload-{index}")),
size: dimensions,
}),
AtlasTextureKind::Polychrome => AtlasKey::Image(RenderImageParams {
image_id: ImageId(index),
frame_index: 0,
}),
AtlasTextureKind::Subpixel => unreachable!(),
};
let mut bytes = Vec::new();
for row in 0..dimensions.height.0 {
for column in 0..dimensions.width.0 {
let value = (index as u8)
.wrapping_mul(73)
.wrapping_add((row as u8).wrapping_mul(31))
.wrapping_add((column as u8).wrapping_mul(17));
match kind {
AtlasTextureKind::Monochrome => bytes.push(value),
AtlasTextureKind::Polychrome => {
bytes.extend([value, value.wrapping_add(53), value.wrapping_add(107), 255])
}
AtlasTextureKind::Subpixel => unreachable!(),
}
}
}
Entry { key, bytes }
}
fn tile_scene(tile: AtlasTile) -> Scene {
let bounds = Bounds {
origin: Point::default(),
size: size(
ScaledPixels(tile.bounds.size.width.0 as f32),
ScaledPixels(tile.bounds.size.height.0 as f32),
),
};
let content_mask = ContentMask { bounds };
let mut scene = Scene::default();
match tile.texture_id.kind {
AtlasTextureKind::Monochrome => scene.insert_primitive(MonochromeSprite {
order: 0,
pad: 0,
tile,
bounds,
content_mask,
color: Hsla {
h: 0.0,
s: 0.0,
l: 1.0,
a: 1.0,
},
transformation: TransformationMatrix::default(),
}),
AtlasTextureKind::Polychrome => scene.insert_primitive(PolychromeSprite {
order: 0,
pad: 0,
tile,
grayscale: PaddedBool32::from(false),
opacity: 1.0,
bounds,
content_mask,
corner_radii: Corners::default(),
}),
AtlasTextureKind::Subpixel => unreachable!(),
}
scene.finish();
scene
}
gpui::bench_group! {
name = benches;
config = Criterion::default()
.sample_size(30)
.warm_up_time(Duration::from_secs(1))
.measurement_time(Duration::from_secs(2))
.without_plots();
targets = atlas_upload
}
gpui::bench_main!(benches);
+1 -1
View File
@@ -118,7 +118,7 @@ pub fn current_headless_renderer() -> anyhow::Result<Option<Box<dyn gpui::Platfo
#[cfg(target_os = "macos")]
{
Ok(Some(Box::new(
gpui_macos::metal_renderer::MetalHeadlessRenderer::new(),
gpui_macos::metal_renderer::MetalHeadlessRenderer::new()?,
)))
}
+71 -18
View File
@@ -16,18 +16,18 @@ mod tests {
};
use anyhow::{Context as _, Result};
use cargo_metadata::{DependencyKind, Metadata, MetadataCommand};
use cargo_metadata::{CargoOpt, DependencyKind, Metadata, MetadataCommand};
use super::load_workspace;
/// Direct dependencies forbidden in workspace packages, paired with the reason shown on failure.
const FORBIDDEN_DIRECT_DEPENDENCIES: &[(&str, &str)] = &[
const FORBIDDEN_OBJC_DEPENDENCIES: &[(&str, &str)] = &[
("block", "use `block2` instead"),
(
"cocoa",
"use `objc2-app-kit` and `objc2-foundation` instead",
),
("cocoa-foundation", "use `objc2-foundation` instead"),
("metal", "use `objc2-metal` and `objc2-quartz-core` instead"),
("objc", "use `objc2` instead"),
("objc-foundation", "use `objc2-foundation` instead"),
("objc_id", "use `objc2::rc::Retained` instead"),
@@ -53,21 +53,24 @@ mod tests {
];
#[test]
fn no_forbidden_direct_dependencies() -> Result<()> {
let workspace = load_workspace()?;
let violations = forbidden_direct_dependency_violations(&workspace)?;
fn no_forbidden_objc_dependencies() -> Result<()> {
let workspace = MetadataCommand::new()
.features(CargoOpt::AllFeatures)
.exec()
.context("loading cargo metadata with all workspace features")?;
let violations = forbidden_objc_dependency_violations(&workspace)?;
assert_eq!(
violations,
Vec::<String>::new(),
"forbidden direct dependencies:\n{}",
"forbidden Objective-C dependencies:\n{}",
violations.join("\n"),
);
Ok(())
}
#[test]
fn forbidden_direct_dependencies_cover_manifest_declarations() -> Result<()> {
fn forbidden_objc_dependencies_cover_manifests_and_transitive_packages() -> Result<()> {
let fixture = tempfile::tempdir()?;
for name in [
"block",
@@ -80,16 +83,21 @@ mod tests {
"objc2",
"objc2-app-kit",
"objc2-foundation",
"objc2-metal",
"objc2-quartz-core",
"metal",
"intermediate",
"workspace",
] {
let directory = fixture.path().join(name);
fs::create_dir(&directory)?;
fs::write(directory.join("lib.rs"), "")?;
let dependencies = if name == "metal" {
"[dependencies]\nobjc = { path = '../objc' }\n"
} else {
""
let dependencies = match name {
"metal" => "[dependencies]\nobjc = { path = '../objc' }\n",
"intermediate" => {
"[dependencies]\nmetal = { path = '../metal', optional = true }\n"
}
_ => "",
};
fs::write(
directory.join("Cargo.toml"),
@@ -110,7 +118,8 @@ block2 = { path = "../block2" }
objc2 = { path = "../objc2" }
objc2-app-kit = { path = "../objc2-app-kit" }
objc2-foundation = { path = "../objc2-foundation" }
metal = { path = "../metal" }
objc2-metal = { path = "../objc2-metal" }
objc2-quartz-core = { path = "../objc2-quartz-core" }
"#,
);
let forbidden = r#"
@@ -122,6 +131,9 @@ path = "../cocoa"
[dependencies.cocoa-foundation]
path = "../cocoa-foundation"
optional = true
[dependencies.legacy_metal]
package = "metal"
path = "../metal"
[target.'cfg(any())'.dependencies]
objc = { path = "../objc" }
[dev-dependencies]
@@ -132,6 +144,7 @@ legacy_id = { workspace = true, optional = true }
legacy_id = { package = "objc_id", path = "../objc_id" }
objc = "0.1.0"
unused_cocoa = { package = "cocoa", version = "0.1.0" }
unused_metal = { package = "metal", version = "0.1.0" }
unused_objc2 = { package = "objc2", version = "0.1.0" }
"#;
fs::write(&manifest, format!("{allowed}{forbidden}"))?;
@@ -139,7 +152,7 @@ unused_objc2 = { package = "objc2", version = "0.1.0" }
command
.manifest_path(&manifest)
.other_options(vec!["--offline".to_owned()]);
let mut violations = forbidden_direct_dependency_violations(&command.exec()?)?;
let mut violations = forbidden_objc_dependency_violations(&command.exec()?)?;
violations.sort();
assert_eq!(
violations,
@@ -147,18 +160,42 @@ unused_objc2 = { package = "objc2", version = "0.1.0" }
"Cargo.toml [workspace.dependencies]: `legacy_id` uses `objc_id`; use `objc2::rc::Retained` instead",
"Cargo.toml [workspace.dependencies]: `objc` uses `objc`; use `objc2` instead",
"Cargo.toml [workspace.dependencies]: `unused_cocoa` uses `cocoa`; use `objc2-app-kit` and `objc2-foundation` instead",
"Cargo.toml [workspace.dependencies]: `unused_metal` uses `metal`; use `objc2-metal` and `objc2-quartz-core` instead",
"workspace directly depends on `block`; use `block2` instead",
"workspace directly depends on `cocoa-foundation`; use `objc2-foundation` instead",
"workspace directly depends on `cocoa`; use `objc2-app-kit` and `objc2-foundation` instead",
"workspace directly depends on `metal`; use `objc2-metal` and `objc2-quartz-core` instead",
"workspace directly depends on `objc-foundation`; use `objc2-foundation` instead",
"workspace directly depends on `objc_id`; use `objc2::rc::Retained` instead",
"workspace directly depends on `objc`; use `objc2` instead",
],
"including renamed optional inactive-target build dependency",
);
fs::write(
&manifest,
format!(
"{allowed}\n[target.'cfg(any())'.dependencies]\n\
intermediate = {{ path = '../intermediate', optional = true }}\n\
[features]\nlegacy = ['dep:intermediate', 'intermediate/metal']\n"
),
)?;
assert_eq!(
forbidden_objc_dependency_violations(&command.exec()?)?,
Vec::<String>::new(),
);
command.features(CargoOpt::AllFeatures);
let mut violations = forbidden_objc_dependency_violations(&command.exec()?)?;
violations.sort();
assert_eq!(
violations,
[
"dependency graph contains `metal`; use `objc2-metal` and `objc2-quartz-core` instead",
"dependency graph contains `objc`; use `objc2` instead",
],
);
fs::write(&manifest, allowed)?;
assert_eq!(
forbidden_direct_dependency_violations(&command.exec()?)?,
forbidden_objc_dependency_violations(&command.exec()?)?,
Vec::<String>::new(),
);
Ok(())
@@ -240,11 +277,12 @@ unused_objc2 = { package = "objc2", version = "0.1.0" }
);
}
fn forbidden_direct_dependency_violations(workspace: &Metadata) -> Result<Vec<String>> {
fn forbidden_objc_dependency_violations(workspace: &Metadata) -> Result<Vec<String>> {
let mut violations = Vec::new();
let mut reported_dependencies = BTreeSet::new();
for package in workspace.workspace_packages() {
for dependency in &package.dependencies {
let Some((_, reason)) = FORBIDDEN_DIRECT_DEPENDENCIES
let Some((_, reason)) = FORBIDDEN_OBJC_DEPENDENCIES
.iter()
.find(|(name, _)| *name == dependency.name)
else {
@@ -254,6 +292,21 @@ unused_objc2 = { package = "objc2", version = "0.1.0" }
"{} directly depends on `{}`; {reason}",
package.name, dependency.name
));
reported_dependencies.insert(dependency.name.as_str());
}
}
for package in &workspace.packages {
let Some((_, reason)) = FORBIDDEN_OBJC_DEPENDENCIES
.iter()
.find(|(name, _)| *name == package.name.as_str())
else {
continue;
};
if reported_dependencies.insert(package.name.as_str()) {
violations.push(format!(
"dependency graph contains `{}`; {reason}",
package.name,
));
}
}
@@ -271,7 +324,7 @@ unused_objc2 = { package = "objc2", version = "0.1.0" }
.get("package")
.and_then(toml::Value::as_str)
.unwrap_or(alias);
let Some((_, reason)) = FORBIDDEN_DIRECT_DEPENDENCIES
let Some((_, reason)) = FORBIDDEN_OBJC_DEPENDENCIES
.iter()
.find(|(forbidden, _)| *forbidden == name)
else {