@@ -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,110 @@ public enum OperatingSystem: Hashable, Sendable {
167227 return . elf
168228 }
169229 }
230+
231+ private func detectHostLinuxDistribution( ) -> LinuxDistribution ? {
232+ return detectHostLinuxDistribution ( fs: localFS)
233+ }
234+
235+ /// Detects the Linux distribution by examining system files with an injected filesystem
236+ /// Start with the "generic" /etc/os-release then fallback
237+ /// to various distribution named files.
238+ public func detectHostLinuxDistribution( fs: any FSProxy ) -> LinuxDistribution ? {
239+ // Try /etc/os-release first (standard)
240+ let osReleasePath = Path ( " /etc/os-release " )
241+ if fs. exists ( osReleasePath) {
242+ if let osReleaseData = try ? fs. read ( osReleasePath) ,
243+ let osRelease = String ( data: Data ( osReleaseData. bytes) , encoding: . utf8) {
244+ if let distribution = parseOSRelease ( osRelease) {
245+ return distribution
246+ }
247+ }
248+ }
249+
250+ // Fallback to distribution-specific files
251+ let distributionFiles : [ ( String , LinuxDistribution . Kind ) ] = [
252+ ( " /etc/ubuntu-release " , . ubuntu) ,
253+ ( " /etc/debian_version " , . debian) ,
254+ ( " /etc/amazon-release " , . amazon) ,
255+ ( " /etc/centos-release " , . centos) ,
256+ ( " /etc/redhat-release " , . rhel) ,
257+ ( " /etc/fedora-release " , . fedora) ,
258+ ( " /etc/SuSE-release " , . suse) ,
259+ ( " /etc/alpine-release " , . alpine) ,
260+ ( " /etc/arch-release " , . arch) ,
261+ ]
262+
263+ for (file, kind) in distributionFiles {
264+ if fs. exists ( Path ( file) ) {
265+ return LinuxDistribution ( kind: kind)
266+ }
267+ }
268+
269+ return nil
270+ }
271+
272+ /// Parses /etc/os-release content to determine distribution and version
273+ /// Fallback to just getting the distribution from specific files.
274+ private func parseOSRelease( _ content: String ) -> LinuxDistribution ? {
275+ let lines = content. components ( separatedBy: . newlines)
276+ var id : String ?
277+ var idLike : String ?
278+ var versionId : String ?
279+
280+ // Parse out ID, ID_LIKE and VERSION_ID
281+ for line in lines {
282+ let trimmed = line. trimmingCharacters ( in: . whitespaces)
283+ if trimmed. hasPrefix ( " ID= " ) {
284+ id = String ( trimmed. dropFirst ( 3 ) ) . trimmingCharacters ( in: CharacterSet ( charactersIn: " \" " ) )
285+ } else if trimmed. hasPrefix ( " ID_LIKE= " ) {
286+ idLike = String ( trimmed. dropFirst ( 8 ) ) . trimmingCharacters ( in: CharacterSet ( charactersIn: " \" " ) )
287+ } else if trimmed. hasPrefix ( " VERSION_ID= " ) {
288+ versionId = String ( trimmed. dropFirst ( 11 ) ) . trimmingCharacters ( in: CharacterSet ( charactersIn: " \" " ) )
289+ }
290+ }
291+
292+ // Check ID first
293+ if let id = id {
294+ let kind : LinuxDistribution . Kind ?
295+ switch id. lowercased ( ) {
296+ case " ubuntu " : kind = . ubuntu
297+ case " debian " : kind = . debian
298+ case " amzn " : kind = . amazon
299+ case " centos " : kind = . centos
300+ case " rhel " : kind = . rhel
301+ case " fedora " : kind = . fedora
302+ case " suse " , " opensuse " , " opensuse-leap " , " opensuse-tumbleweed " : kind = . suse
303+ case " alpine " : kind = . alpine
304+ case " arch " : kind = . arch
305+ default : kind = nil
306+ }
307+
308+ if let kind = kind {
309+ return LinuxDistribution ( kind: kind, version: versionId)
310+ }
311+ }
312+
313+ // Check ID_LIKE as fallback
314+ if let idLike = idLike {
315+ let likes = idLike. components ( separatedBy: . whitespaces)
316+ for like in likes {
317+ let kind : LinuxDistribution . Kind ?
318+ switch like. lowercased ( ) {
319+ case " ubuntu " : kind = . ubuntu
320+ case " debian " : kind = . debian
321+ case " rhel " , " fedora " : kind = . rhel
322+ case " suse " : kind = . suse
323+ case " arch " : kind = . arch
324+ default : kind = nil
325+ }
326+
327+ if let kind = kind {
328+ return LinuxDistribution ( kind: kind, version: versionId)
329+ }
330+ }
331+ }
332+ return nil
333+ }
170334}
171335
172336public enum ImageFormat {
@@ -255,3 +419,4 @@ extension FixedWidthInteger {
255419 return self != 0 ? self : other
256420 }
257421}
422+
0 commit comments