@@ -123,6 +123,56 @@ extension ProcessInfo {
123123 return . unknown
124124 #endif
125125 }
126+
127+
128+ }
129+
130+ public struct LinuxDistribution : Hashable , Sendable {
131+ public enum Kind : String , CaseIterable , Hashable , Sendable {
132+ case unknown
133+ case ubuntu
134+ case debian
135+ case amazon = " amzn "
136+ case centos
137+ case rhel
138+ case fedora
139+ case suse
140+ case alpine
141+ case arch
142+
143+ /// The display name for the distribution kind
144+ public var displayName : String {
145+ switch self {
146+ case . unknown: return " Unknown Linux "
147+ case . ubuntu: return " Ubuntu "
148+ case . debian: return " Debian "
149+ case . amazon: return " Amazon Linux "
150+ case . centos: return " CentOS "
151+ case . rhel: return " Red Hat Enterprise Linux "
152+ case . fedora: return " Fedora "
153+ case . suse: return " SUSE "
154+ case . alpine: return " Alpine Linux "
155+ case . arch: return " Arch Linux "
156+ }
157+ }
158+ }
159+
160+ public let kind : Kind
161+ public let version : String ?
162+
163+ public init ( kind: Kind , version: String ? = nil ) {
164+ self . kind = kind
165+ self . version = version
166+ }
167+
168+ /// The display name for the distribution including version if available
169+ public var displayName : String {
170+ if let version = version {
171+ return " \( kind. displayName) \( version) "
172+ } else {
173+ return kind. displayName
174+ }
175+ }
126176}
127177
128178public enum OperatingSystem : Hashable , Sendable {
@@ -157,6 +207,16 @@ public enum OperatingSystem: Hashable, Sendable {
157207 }
158208 }
159209
210+ /// The distribution if this is a Linux operating system
211+ public var distribution : LinuxDistribution ? {
212+ switch self {
213+ case . linux:
214+ return detectHostLinuxDistribution ( )
215+ default :
216+ return nil
217+ }
218+ }
219+
160220 public var imageFormat : ImageFormat {
161221 switch self {
162222 case . macOS, . iOS, . tvOS, . watchOS, . visionOS:
@@ -167,6 +227,101 @@ public enum OperatingSystem: Hashable, Sendable {
167227 return . elf
168228 }
169229 }
230+
231+ /// Detects the Linux distribution by examining system files
232+ /// Start with the "generic" /etc/os-release then fallback
233+ /// to various distribution named files.
234+ private func detectHostLinuxDistribution( ) -> LinuxDistribution ? {
235+ #if os(Linux)
236+ // Try /etc/os-release first (standard)
237+ if let osRelease = try ? String ( contentsOfFile: " /etc/os-release " ) {
238+ if let distribution = parseOSRelease ( osRelease) {
239+ return distribution
240+ }
241+ }
242+ // Fallback to distribution-specific files
243+ let distributionFiles : [ ( String , LinuxDistribution . Kind ) ] = [
244+ ( " /etc/ubuntu-release " , . ubuntu) ,
245+ ( " /etc/debian_version " , . debian) ,
246+ ( " /etc/amazon-release " , . amazon) ,
247+ ( " /etc/centos-release " , . centos) ,
248+ ( " /etc/redhat-release " , . rhel) ,
249+ ( " /etc/fedora-release " , . fedora) ,
250+ ( " /etc/SuSE-release " , . suse) ,
251+ ( " /etc/alpine-release " , . alpine) ,
252+ ( " /etc/arch-release " , . arch) ,
253+ ]
254+ for (file, kind) in distributionFiles {
255+ if FileManager . default. fileExists ( atPath: file) {
256+ return LinuxDistribution ( kind: kind)
257+ }
258+ }
259+ #endif
260+ return nil
261+ }
262+
263+ /// Parses /etc/os-release content to determine distribution and version
264+ /// Fallback to just getting the distribution from specific files.
265+ private func parseOSRelease( _ content: String ) -> LinuxDistribution ? {
266+ let lines = content. components ( separatedBy: . newlines)
267+ var id : String ?
268+ var idLike : String ?
269+ var versionId : String ?
270+
271+ // Parse out ID, ID_LIKE and VERSION_ID
272+ for line in lines {
273+ let trimmed = line. trimmingCharacters ( in: . whitespaces)
274+ if trimmed. hasPrefix ( " ID= " ) {
275+ id = String ( trimmed. dropFirst ( 3 ) ) . trimmingCharacters ( in: CharacterSet ( charactersIn: " \" " ) )
276+ } else if trimmed. hasPrefix ( " ID_LIKE= " ) {
277+ idLike = String ( trimmed. dropFirst ( 8 ) ) . trimmingCharacters ( in: CharacterSet ( charactersIn: " \" " ) )
278+ } else if trimmed. hasPrefix ( " VERSION_ID= " ) {
279+ versionId = String ( trimmed. dropFirst ( 11 ) ) . trimmingCharacters ( in: CharacterSet ( charactersIn: " \" " ) )
280+ }
281+ }
282+
283+ // Check ID first
284+ if let id = id {
285+ let kind : LinuxDistribution . Kind ?
286+ switch id. lowercased ( ) {
287+ case " ubuntu " : kind = . ubuntu
288+ case " debian " : kind = . debian
289+ case " amzn " : kind = . amazon
290+ case " centos " : kind = . centos
291+ case " rhel " : kind = . rhel
292+ case " fedora " : kind = . fedora
293+ case " suse " , " opensuse " , " opensuse-leap " , " opensuse-tumbleweed " : kind = . suse
294+ case " alpine " : kind = . alpine
295+ case " arch " : kind = . arch
296+ default : kind = nil
297+ }
298+
299+ if let kind = kind {
300+ return LinuxDistribution ( kind: kind, version: versionId)
301+ }
302+ }
303+
304+ // Check ID_LIKE as fallback
305+ if let idLike = idLike {
306+ let likes = idLike. components ( separatedBy: . whitespaces)
307+ for like in likes {
308+ let kind : LinuxDistribution . Kind ?
309+ switch like. lowercased ( ) {
310+ case " ubuntu " : kind = . ubuntu
311+ case " debian " : kind = . debian
312+ case " rhel " , " fedora " : kind = . rhel
313+ case " suse " : kind = . suse
314+ case " arch " : kind = . arch
315+ default : kind = nil
316+ }
317+
318+ if let kind = kind {
319+ return LinuxDistribution ( kind: kind, version: versionId)
320+ }
321+ }
322+ }
323+ return nil
324+ }
170325}
171326
172327public enum ImageFormat {
0 commit comments