[llvm] [SystemZ][z/OS] Add z/OS archive writing support (PR #200087)

Amy Kwan via llvm-commits llvm-commits at lists.llvm.org
Mon Jul 13 13:49:56 PDT 2026


https://github.com/amy-kwan updated https://github.com/llvm/llvm-project/pull/200087

>From ddffa75edc99a5f73aeaf5db60c4da13dde6023e Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Wed, 27 May 2026 18:56:30 -0500
Subject: [PATCH 1/9] [SystemZ][z/OS] Add z/OS archive writing support

This patch implement z/OS archive writing in ArchiveWriter and adds the option
`llvm-ar --format=zos`.

Moreover, This patch teaches `llvm-ar` to emit z/OS-compatible archives by:
- Detecting GOFF object files as z/OS members
- Writing z/OS member headers and archive magic
- Converting archive headers and symbol-name string tables to EBCDIC
- Emitting z/OS symbol table entries
- Using EBCDIC newline padding bytes
- Add a z/OS-specific workaround for empty symbol tables by emitting a dummy
  blank symbol to satisfy binder requirements.
---
 llvm/lib/Object/ArchiveWriter.cpp      | 75 ++++++++++++++++++++++++--
 llvm/test/tools/llvm-ar/zos-write.test | 36 +++++++++++++
 llvm/tools/llvm-ar/llvm-ar.cpp         | 18 ++++++-
 3 files changed, 125 insertions(+), 4 deletions(-)
 create mode 100644 llvm/test/tools/llvm-ar/zos-write.test

diff --git a/llvm/lib/Object/ArchiveWriter.cpp b/llvm/lib/Object/ArchiveWriter.cpp
index 3b2d8c30bb301..8479340af26a3 100644
--- a/llvm/lib/Object/ArchiveWriter.cpp
+++ b/llvm/lib/Object/ArchiveWriter.cpp
@@ -20,6 +20,7 @@
 #include "llvm/Object/COFF.h"
 #include "llvm/Object/COFFImportFile.h"
 #include "llvm/Object/Error.h"
+#include "llvm/Object/GOFFObjectFile.h"
 #include "llvm/Object/IRObjectFile.h"
 #include "llvm/Object/MachO.h"
 #include "llvm/Object/ObjectFile.h"
@@ -70,6 +71,8 @@ object::Archive::Kind NewArchiveMember::detectKindFromObject() const {
     if (isa<object::COFFObjectFile>(**OptionalObject) ||
         isa<object::COFFImportFile>(**OptionalObject))
       return object::Archive::K_COFF;
+    if (isa<object::GOFFObjectFile>(**OptionalObject))
+      return object::Archive::K_ZOS;
     return object::Archive::K_GNU;
   }
 
@@ -186,6 +189,10 @@ static bool isCOFFArchive(object::Archive::Kind Kind) {
   return Kind == object::Archive::K_COFF;
 }
 
+static bool isZOSArchive(object::Archive::Kind Kind) {
+  return Kind == object::Archive::K_ZOS;
+}
+
 static bool isBSDLike(object::Archive::Kind Kind) {
   switch (Kind) {
   case object::Archive::K_GNU:
@@ -253,6 +260,24 @@ printBSDMemberHeader(raw_ostream &Out, uint64_t Pos, StringRef Name,
     Out.write(uint8_t(0));
 }
 
+static void
+printZOSMemberHeader(raw_ostream &Out, uint64_t Pos, StringRef Name,
+                     const sys::TimePoint<std::chrono::seconds> &ModTime,
+                     unsigned UID, unsigned GID, unsigned Perms,
+                     uint64_t Size) {
+  std::string AHeader;
+  raw_string_ostream AOut(AHeader);
+  if (Name.size() <= 16) {
+    printWithSpacePadding(AOut, Twine(Name), 16);
+    printRestOfMemberHeader(AOut, ModTime, UID, GID, Perms, Size);
+  } else {
+    printBSDMemberHeader(AOut, Pos, Name, ModTime, UID, GID, Perms, Size);
+  }
+  SmallString<256> EHeader;
+  ConverterEBCDIC::convertToEBCDIC(AHeader, EHeader);
+  Out << EHeader.str();
+}
+
 static void
 printBigArchiveMemberHeader(raw_ostream &Out, StringRef Name,
                             const sys::TimePoint<std::chrono::seconds> &ModTime,
@@ -306,6 +331,9 @@ printMemberHeader(raw_ostream &Out, uint64_t Pos, raw_ostream &StringTable,
   if (isBSDLike(Kind))
     return printBSDMemberHeader(Out, Pos, MemberName, ModTime, M.UID, M.GID,
                                 M.Perms, Size);
+  if (isZOSArchive(Kind))
+    return printZOSMemberHeader(Out, Pos, MemberName, ModTime, M.UID, M.GID,
+                                M.Perms, Size);
   if (!useStringTable(Thin, MemberName))
     return printGNUSmallMemberHeader(Out, MemberName, ModTime, M.UID, M.GID,
                                      M.Perms, Size);
@@ -392,8 +420,11 @@ static uint64_t computeSymbolTableSize(object::Archive::Kind Kind,
   uint64_t Size = OffsetSize; // Number of entries
   if (isBSDLike(Kind))
     Size += NumSyms * OffsetSize * 2; // Table
+  else if (isZOSArchive(Kind))
+    Size += NumSyms * OffsetSize * 2; // MemberOffset + symbol flags
   else
     Size += NumSyms * OffsetSize; // Table
+
   if (isBSDLike(Kind))
     Size += OffsetSize; // byte count
   Size += StringTableSize;
@@ -453,6 +484,10 @@ static void writeSymbolTableHeader(raw_ostream &Out, object::Archive::Kind Kind,
   } else if (isAIXBigArchive(Kind)) {
     printBigArchiveMemberHeader(Out, "", now(Deterministic), 0, 0, 0, Size,
                                 PrevMemberOffset, NextMemberOffset);
+  } else if (isZOSArchive(Kind)) {
+    const char *Name = "__.SYMDEF";
+    printZOSMemberHeader(Out, Out.tell(), Name, now(Deterministic), 0, 0, 0,
+                         Size);
   } else {
     const char *Name = is64BitKind(Kind) ? "/SYM64" : "";
     printGNUSmallMemberHeader(Out, Name, now(Deterministic), 0, 0, 0, Size);
@@ -613,6 +648,10 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
                                          StringTable.size(), &Pad);
   writeSymbolTableHeader(Out, Kind, Deterministic, Size, PrevMemberOffset,
                          NextMemberOffset);
+  // Padding size is not included in the Size field of the z/OS symbol table
+  // header
+  if (isZOSArchive(Kind))
+    Size -= Pad;
 
   if (isBSDLike(Kind))
     printNBits(Out, Kind, NumSyms * 2 * OffsetSize);
@@ -633,6 +672,8 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
       if (isBSDLike(Kind))
         printNBits(Out, Kind, StringOffset);
       printNBits(Out, Kind, Pos); // member offset
+      if (isZOSArchive(Kind))
+        printNBits(Out, Kind, Pos); // symbol flags
     }
     Pos += M.Header.size() + M.Data.size() + M.Padding.size();
   }
@@ -640,7 +681,16 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
   if (isBSDLike(Kind))
     // byte count of the string table
     printNBits(Out, Kind, StringTable.size());
-  Out << StringTable;
+  if (isZOSArchive(Kind)) {
+    std::string AStringTable;
+    raw_string_ostream AOut(AStringTable);
+    AOut << StringTable;
+    SmallString<256> EStringTable;
+    ConverterEBCDIC::convertToEBCDIC(AStringTable, EStringTable);
+    Out << EStringTable.str();
+  } else {
+    Out << StringTable;
+  }
 
   while (Pad--)
     Out.write(uint8_t(0));
@@ -785,6 +835,8 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
                   LLVMContext &Context, ArrayRef<NewArchiveMember> NewMembers,
                   std::optional<bool> IsEC, function_ref<void(Error)> Warn) {
   static char PaddingData[8] = {'\n', '\n', '\n', '\n', '\n', '\n', '\n', '\n'};
+  static char ZOSPaddingData[8] = {0x15, 0x15, 0x15, 0x15,
+                                   0x15, 0x15, 0x15, 0x15}; // EBCDIC newlines.
   uint64_t Pos =
       isAIXBigArchive(Kind) ? sizeof(object::BigArchive::FixLenHdr) : 0;
 
@@ -846,6 +898,9 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
       Entry.second = Entry.second > 1 ? 1 : 0;
   }
 
+  uint32_t LastZosObjIndex =
+      UINT_MAX; // Only set when writing symbol table in z/OS archive.
+
   for (const NewArchiveMember &M : NewMembers) {
     MemberData &D = Ret.emplace_back();
     D.Data = M.Buf->getBuffer();
@@ -858,7 +913,6 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
       if (!SymFileOrErr)
         return createFileError(M.MemberName, SymFileOrErr.takeError());
       D.SymFile = std::move(*SymFileOrErr);
-
       if (SymMap && D.SymFile.get()) {
         auto COFFObj = dyn_cast<COFFObjectFile>(D.SymFile.get());
         std::optional<MemoryBufferRef> HybridView;
@@ -887,6 +941,9 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
                                   .str();
         }
       }
+
+      if (isZOSArchive(Kind) && D.SymFile.get())
+        LastZosObjIndex = Ret.size() - 1;
     }
   }
 
@@ -941,7 +998,8 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
         isDarwin(Kind) ? offsetToAlignment(D.Data.size(), Align(8)) : 0;
     unsigned TailPadding =
         offsetToAlignment(D.Data.size() + MemberPadding, Align(2));
-    D.Padding = StringRef(PaddingData, MemberPadding + TailPadding);
+    D.Padding = StringRef(isZOSArchive(Kind) ? ZOSPaddingData : PaddingData,
+                          MemberPadding + TailPadding);
 
     StringRef MemberName = D.HybridName.size() ? D.HybridName : M->MemberName;
 
@@ -1006,6 +1064,15 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
       if (D.SymFile)
         HasObject = true;
     }
+    // On z/OS, when there are no symbols, add a dummy blank symbol
+    // into the symbol table. This is done since the z/OS binder:
+    //   - emits an error if there is no symbol table in the archive
+    //   - emits an error if the symbol table has 0 symbols
+    //   - should not find any references to a blank symbol
+    if ((LastZosObjIndex == Index) && (SymNames.tell() == 0)) {
+      D.Symbols.push_back(0);
+      SymNames << ' ' << '\0';
+    }
 
     Pos += D.Header.size() + D.Data.size() + D.Padding.size();
   }
@@ -1180,6 +1247,8 @@ Error writeArchiveToStream(raw_ostream &Out,
     Out << "!<thin>\n";
   else if (isAIXBigArchive(Kind))
     Out << "<bigaf>\n";
+  else if (isZOSArchive(Kind))
+    Out << ZOSArchiveMagic;
   else
     Out << "!<arch>\n";
 
diff --git a/llvm/test/tools/llvm-ar/zos-write.test b/llvm/test/tools/llvm-ar/zos-write.test
new file mode 100644
index 0000000000000..447e4562b83aa
--- /dev/null
+++ b/llvm/test/tools/llvm-ar/zos-write.test
@@ -0,0 +1,36 @@
+# Test writing a z/OS archive.
+
+# Create a z/OS archive from a plain text file, without a symbol table.
+# RUN: rm -rf %t.dir && mkdir -p %t.dir
+# RUN: printf 'abcd' > %t.dir/test.txt
+# RUN: llvm-ar rcSD --format=zos %t.a %t.dir/test.txt
+
+# The archive should be written and read back successfully through llvm-ar.
+# RUN: llvm-ar t %t.a | FileCheck %s --check-prefix=LIST
+# RUN: llvm-ar p %t.a test.txt | FileCheck %s --check-prefix=CONTENT
+
+# Check raw bytes:
+# - The first 8 bytes are the z/OS archive magic in EBCDIC.
+# - Bytes 66..67 are the first member header terminator (`\n in EBCDIC => 79 15)
+# RUN: %python -c "d=open(r'%t.a','rb').read(); print(d[:8].hex()); print(d[66:68].hex())" | FileCheck %s --check-prefix=BYTES
+
+# LIST: test.txt
+# CONTENT: abcd
+# BYTES: 5a4c819983886e15
+# BYTES-NEXT: 7915
+
+# Odd-sized members should be padded with EBCDIC newline (0x15).
+# RUN: printf 'abc' > %t.dir/odd.txt
+# RUN: llvm-ar rcSD --format=zos %t.odd.a %t.dir/odd.txt
+# RUN: %python -c "d=open(r'%t.odd.a','rb').read(); print(f'{d[-1]:02x}')" | FileCheck %s --check-prefix=PAD
+
+# PAD: 15
+
+# Long member names should written and read back correctly.
+# RUN: printf 'xyz' > %t.dir/very_long_member_name.txt
+# RUN: llvm-ar rcSD --format=zos %t.long.a %t.dir/very_long_member_name.txt
+# RUN: llvm-ar t %t.long.a | FileCheck %s --check-prefix=LONG-LIST
+# RUN: llvm-ar p %t.long.a very_long_member_name.txt | FileCheck %s --check-prefix=LONG-CONTENT
+
+# LONG-LIST: very_long_member_name.txt
+# LONG-CONTENT: xyz
diff --git a/llvm/tools/llvm-ar/llvm-ar.cpp b/llvm/tools/llvm-ar/llvm-ar.cpp
index bac99c548a178..38f929f63b39f 100644
--- a/llvm/tools/llvm-ar/llvm-ar.cpp
+++ b/llvm/tools/llvm-ar/llvm-ar.cpp
@@ -83,6 +83,7 @@ static void printArHelp(StringRef ToolName) {
     =darwin             -   darwin
     =bsd                -   bsd
     =bigarchive         -   big archive (AIX OS)
+    =zos                -   zos archive (z/OS OS)
     =coff               -   coff
   --plugin=<string>     - ignored for compatibility
   -h --help             - display this help and exit
@@ -195,7 +196,16 @@ static SmallVector<const char *, 256> PositionalArgs;
 static bool MRI;
 
 namespace {
-enum Format { Default, GNU, COFF, BSD, DARWIN, BIGARCHIVE, Unknown };
+enum Format {
+  Default,
+  GNU,
+  COFF,
+  BSD,
+  DARWIN,
+  BIGARCHIVE,
+  ZOSARCHIVE,
+  Unknown
+};
 }
 
 static Format FormatType = Default;
@@ -1079,6 +1089,11 @@ static void performWriteOperation(ArchiveOperation Operation,
       fail("only the gnu format has a thin mode");
     Kind = object::Archive::K_AIXBIG;
     break;
+  case ZOSARCHIVE:
+    if (Thin)
+      fail("only the gnu format has a thin mode");
+    Kind = object::Archive::K_ZOS;
+    break;
   case Unknown:
     llvm_unreachable("");
   }
@@ -1397,6 +1412,7 @@ static int ar_main(int argc, char **argv) {
                        .Case("bsd", BSD)
                        .Case("bigarchive", BIGARCHIVE)
                        .Case("coff", COFF)
+                       .Case("zos", ZOSARCHIVE)
                        .Default(Unknown);
       if (FormatType == Unknown)
         fail(std::string("Invalid format ") + Match);

>From 37ad1267dc5ba2ed0925dfadffc8405fdfc39e4f Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Fri, 12 Jun 2026 07:11:24 -0400
Subject: [PATCH 2/9] Address review comments.

---
 llvm/lib/Object/ArchiveWriter.cpp      | 14 +++++------
 llvm/test/tools/llvm-ar/zos-write.test | 34 ++++++++++++++++++++------
 2 files changed, 32 insertions(+), 16 deletions(-)

diff --git a/llvm/lib/Object/ArchiveWriter.cpp b/llvm/lib/Object/ArchiveWriter.cpp
index 8479340af26a3..3bfb1d06c7056 100644
--- a/llvm/lib/Object/ArchiveWriter.cpp
+++ b/llvm/lib/Object/ArchiveWriter.cpp
@@ -418,10 +418,11 @@ static uint64_t computeSymbolTableSize(object::Archive::Kind Kind,
                                        uint32_t *Padding = nullptr) {
   assert((OffsetSize == 4 || OffsetSize == 8) && "Unsupported OffsetSize");
   uint64_t Size = OffsetSize; // Number of entries
-  if (isBSDLike(Kind))
+  // Each symbol table entry consists of a member offset.
+  // For BSD, each entry also includes a string table offset.
+  // For z/OS, each entry also includes a flag field.
+  if (isBSDLike(Kind) || isZOSArchive(Kind))
     Size += NumSyms * OffsetSize * 2; // Table
-  else if (isZOSArchive(Kind))
-    Size += NumSyms * OffsetSize * 2; // MemberOffset + symbol flags
   else
     Size += NumSyms * OffsetSize; // Table
 
@@ -649,7 +650,7 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
   writeSymbolTableHeader(Out, Kind, Deterministic, Size, PrevMemberOffset,
                          NextMemberOffset);
   // Padding size is not included in the Size field of the z/OS symbol table
-  // header
+  // header.
   if (isZOSArchive(Kind))
     Size -= Pad;
 
@@ -682,11 +683,8 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
     // byte count of the string table
     printNBits(Out, Kind, StringTable.size());
   if (isZOSArchive(Kind)) {
-    std::string AStringTable;
-    raw_string_ostream AOut(AStringTable);
-    AOut << StringTable;
     SmallString<256> EStringTable;
-    ConverterEBCDIC::convertToEBCDIC(AStringTable, EStringTable);
+    ConverterEBCDIC::convertToEBCDIC(StringTable, EStringTable);
     Out << EStringTable.str();
   } else {
     Out << StringTable;
diff --git a/llvm/test/tools/llvm-ar/zos-write.test b/llvm/test/tools/llvm-ar/zos-write.test
index 447e4562b83aa..d26b6d092b961 100644
--- a/llvm/test/tools/llvm-ar/zos-write.test
+++ b/llvm/test/tools/llvm-ar/zos-write.test
@@ -1,17 +1,17 @@
-# Test writing a z/OS archive.
+## Test writing a z/OS archive.
 
-# Create a z/OS archive from a plain text file, without a symbol table.
+## Create a z/OS archive from a plain text file, without a symbol table.
 # RUN: rm -rf %t.dir && mkdir -p %t.dir
 # RUN: printf 'abcd' > %t.dir/test.txt
 # RUN: llvm-ar rcSD --format=zos %t.a %t.dir/test.txt
 
-# The archive should be written and read back successfully through llvm-ar.
+## The archive should be written and read back successfully through llvm-ar.
 # RUN: llvm-ar t %t.a | FileCheck %s --check-prefix=LIST
 # RUN: llvm-ar p %t.a test.txt | FileCheck %s --check-prefix=CONTENT
 
-# Check raw bytes:
-# - The first 8 bytes are the z/OS archive magic in EBCDIC.
-# - Bytes 66..67 are the first member header terminator (`\n in EBCDIC => 79 15)
+## Check raw bytes:
+## - The first 8 bytes are the z/OS archive magic in EBCDIC.
+## - Bytes 66..67 are the first member header terminator (`\n in EBCDIC => 79 15)
 # RUN: %python -c "d=open(r'%t.a','rb').read(); print(d[:8].hex()); print(d[66:68].hex())" | FileCheck %s --check-prefix=BYTES
 
 # LIST: test.txt
@@ -19,14 +19,14 @@
 # BYTES: 5a4c819983886e15
 # BYTES-NEXT: 7915
 
-# Odd-sized members should be padded with EBCDIC newline (0x15).
+## Odd-sized members should be padded with EBCDIC newline (0x15).
 # RUN: printf 'abc' > %t.dir/odd.txt
 # RUN: llvm-ar rcSD --format=zos %t.odd.a %t.dir/odd.txt
 # RUN: %python -c "d=open(r'%t.odd.a','rb').read(); print(f'{d[-1]:02x}')" | FileCheck %s --check-prefix=PAD
 
 # PAD: 15
 
-# Long member names should written and read back correctly.
+## Long member names should be written and read back correctly.
 # RUN: printf 'xyz' > %t.dir/very_long_member_name.txt
 # RUN: llvm-ar rcSD --format=zos %t.long.a %t.dir/very_long_member_name.txt
 # RUN: llvm-ar t %t.long.a | FileCheck %s --check-prefix=LONG-LIST
@@ -34,3 +34,21 @@
 
 # LONG-LIST: very_long_member_name.txt
 # LONG-CONTENT: xyz
+
+## A z/OS archive with real symbols should write a readable symbol table.
+# RUN: printf 'define i32 @mytest() {\nentry:\n  ret i32 87\n}\n' > %t.dir/mytest.ll
+# RUN: llvm-as %t.dir/mytest.ll -o %t.dir/mytest.bc
+# RUN: llvm-ar rcD --format=zos %t.sym.a %t.dir/mytest.bc
+# RUN: llvm-nm --print-armap %t.sym.a | FileCheck %s --check-prefix=SYMTAB
+
+# SYMTAB: Archive map
+# SYMTAB-NEXT: mytest in mytest.bc
+
+## A z/OS archive with no real symbols should still write the dummy blank symbol.
+# RUN: printf 'source_filename = "blank.ll"\n' > %t.dir/blank.ll
+# RUN: llvm-as %t.dir/blank.ll -o %t.dir/blank.bc
+# RUN: llvm-ar rcD --format=zos %t.blank-sym.a %t.dir/blank.bc
+# RUN: llvm-nm --print-armap %t.blank-sym.a | FileCheck %s --check-prefix=EMPTY-SYMTAB
+
+# EMPTY-SYMTAB: Archive map
+# EMPTY-SYMTAB-NEXT: {{^  in blank.bc$}}

>From d5918eaac127d742861c9943fa50efa637cdc45f Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Sat, 20 Jun 2026 23:39:42 -0400
Subject: [PATCH 3/9] Address review comments.

---
 llvm/lib/Object/ArchiveWriter.cpp      |  2 +-
 llvm/test/tools/llvm-ar/zos-write.test | 21 +++++++++++++++++----
 2 files changed, 18 insertions(+), 5 deletions(-)

diff --git a/llvm/lib/Object/ArchiveWriter.cpp b/llvm/lib/Object/ArchiveWriter.cpp
index 3bfb1d06c7056..b16f4d8c341d3 100644
--- a/llvm/lib/Object/ArchiveWriter.cpp
+++ b/llvm/lib/Object/ArchiveWriter.cpp
@@ -420,7 +420,7 @@ static uint64_t computeSymbolTableSize(object::Archive::Kind Kind,
   uint64_t Size = OffsetSize; // Number of entries
   // Each symbol table entry consists of a member offset.
   // For BSD, each entry also includes a string table offset.
-  // For z/OS, each entry also includes a flag field.
+  // For z/OS, each entry instead also includes a flag field.
   if (isBSDLike(Kind) || isZOSArchive(Kind))
     Size += NumSyms * OffsetSize * 2; // Table
   else
diff --git a/llvm/test/tools/llvm-ar/zos-write.test b/llvm/test/tools/llvm-ar/zos-write.test
index d26b6d092b961..571964ff566bb 100644
--- a/llvm/test/tools/llvm-ar/zos-write.test
+++ b/llvm/test/tools/llvm-ar/zos-write.test
@@ -1,7 +1,9 @@
 ## Test writing a z/OS archive.
 
-## Create a z/OS archive from a plain text file, without a symbol table.
 # RUN: rm -rf %t.dir && mkdir -p %t.dir
+# RUN: split-file %s %t.dir
+
+## Create a z/OS archive from a plain text file with symbol table writing disabled.
 # RUN: printf 'abcd' > %t.dir/test.txt
 # RUN: llvm-ar rcSD --format=zos %t.a %t.dir/test.txt
 
@@ -11,7 +13,7 @@
 
 ## Check raw bytes:
 ## - The first 8 bytes are the z/OS archive magic in EBCDIC.
-## - Bytes 66..67 are the first member header terminator (`\n in EBCDIC => 79 15)
+## - Bytes 66..67 are the first member header terminator (`\n in EBCDIC => 79 15).
 # RUN: %python -c "d=open(r'%t.a','rb').read(); print(d[:8].hex()); print(d[66:68].hex())" | FileCheck %s --check-prefix=BYTES
 
 # LIST: test.txt
@@ -35,20 +37,31 @@
 # LONG-LIST: very_long_member_name.txt
 # LONG-CONTENT: xyz
 
+## Use LLVM bitcode here so the test remains target-independent. The archive
+## writer only needs a symbolic input in order to emit the archive symbol table.
 ## A z/OS archive with real symbols should write a readable symbol table.
-# RUN: printf 'define i32 @mytest() {\nentry:\n  ret i32 87\n}\n' > %t.dir/mytest.ll
 # RUN: llvm-as %t.dir/mytest.ll -o %t.dir/mytest.bc
 # RUN: llvm-ar rcD --format=zos %t.sym.a %t.dir/mytest.bc
 # RUN: llvm-nm --print-armap %t.sym.a | FileCheck %s --check-prefix=SYMTAB
 
 # SYMTAB: Archive map
 # SYMTAB-NEXT: mytest in mytest.bc
+# SYMTAB-NOT: {{^  in mytest.bc$}}
 
 ## A z/OS archive with no real symbols should still write the dummy blank symbol.
-# RUN: printf 'source_filename = "blank.ll"\n' > %t.dir/blank.ll
 # RUN: llvm-as %t.dir/blank.ll -o %t.dir/blank.bc
 # RUN: llvm-ar rcD --format=zos %t.blank-sym.a %t.dir/blank.bc
 # RUN: llvm-nm --print-armap %t.blank-sym.a | FileCheck %s --check-prefix=EMPTY-SYMTAB
 
 # EMPTY-SYMTAB: Archive map
 # EMPTY-SYMTAB-NEXT: {{^  in blank.bc$}}
+# EMPTY-SYMTAB-NOT: {{^.+ in blank.bc$}}
+
+#--- mytest.ll
+define i32 @mytest() {
+entry:
+  ret i32 87
+}
+
+#--- blank.ll
+source_filename = "blank.ll"

>From 79efc8e8898eab8b5dc1313aaba8b10b57419812 Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Thu, 25 Jun 2026 15:19:35 -0400
Subject: [PATCH 4/9] Remove regex from checks.

---
 llvm/test/tools/llvm-ar/zos-write.test | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/llvm/test/tools/llvm-ar/zos-write.test b/llvm/test/tools/llvm-ar/zos-write.test
index 571964ff566bb..d09d1214a328c 100644
--- a/llvm/test/tools/llvm-ar/zos-write.test
+++ b/llvm/test/tools/llvm-ar/zos-write.test
@@ -46,7 +46,7 @@
 
 # SYMTAB: Archive map
 # SYMTAB-NEXT: mytest in mytest.bc
-# SYMTAB-NOT: {{^  in mytest.bc$}}
+# SYMTAB-NOT: in mytest.bc
 
 ## A z/OS archive with no real symbols should still write the dummy blank symbol.
 # RUN: llvm-as %t.dir/blank.ll -o %t.dir/blank.bc
@@ -54,8 +54,8 @@
 # RUN: llvm-nm --print-armap %t.blank-sym.a | FileCheck %s --check-prefix=EMPTY-SYMTAB
 
 # EMPTY-SYMTAB: Archive map
-# EMPTY-SYMTAB-NEXT: {{^  in blank.bc$}}
-# EMPTY-SYMTAB-NOT: {{^.+ in blank.bc$}}
+# EMPTY-SYMTAB-NEXT: in blank.bc
+# EMPTY-SYMTAB-NOT: in blank.bc
 
 #--- mytest.ll
 define i32 @mytest() {

>From daa1514cb7873fa86b004f6f62627ce18f1b8aa7 Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Fri, 10 Jul 2026 16:46:03 -0400
Subject: [PATCH 5/9] Fix printZOSMemberHeader and handle ConverterEBCDIC error
 codes.

---
 llvm/lib/Object/ArchiveWriter.cpp | 23 ++++++++++++++++-------
 1 file changed, 16 insertions(+), 7 deletions(-)

diff --git a/llvm/lib/Object/ArchiveWriter.cpp b/llvm/lib/Object/ArchiveWriter.cpp
index b16f4d8c341d3..283f2bf0fbd07 100644
--- a/llvm/lib/Object/ArchiveWriter.cpp
+++ b/llvm/lib/Object/ArchiveWriter.cpp
@@ -261,7 +261,7 @@ printBSDMemberHeader(raw_ostream &Out, uint64_t Pos, StringRef Name,
 }
 
 static void
-printZOSMemberHeader(raw_ostream &Out, uint64_t Pos, StringRef Name,
+printZOSMemberHeader(raw_ostream &Out, StringRef Name,
                      const sys::TimePoint<std::chrono::seconds> &ModTime,
                      unsigned UID, unsigned GID, unsigned Perms,
                      uint64_t Size) {
@@ -271,10 +271,17 @@ printZOSMemberHeader(raw_ostream &Out, uint64_t Pos, StringRef Name,
     printWithSpacePadding(AOut, Twine(Name), 16);
     printRestOfMemberHeader(AOut, ModTime, UID, GID, Perms, Size);
   } else {
-    printBSDMemberHeader(AOut, Pos, Name, ModTime, UID, GID, Perms, Size);
+    // z/OS ar stores the exact name length inline with no extra alignment
+    // padding, unlike the BSD format which pads to an 8-byte boundary.
+    printWithSpacePadding(AOut, Twine("#1/") + Twine(Name.size()), 16);
+    printRestOfMemberHeader(AOut, ModTime, UID, GID, Perms,
+                            Name.size() + Size);
+    AOut << Name;
   }
   SmallString<256> EHeader;
-  ConverterEBCDIC::convertToEBCDIC(AHeader, EHeader);
+  if (std::error_code EC = ConverterEBCDIC::convertToEBCDIC(AHeader, EHeader))
+    report_fatal_error(Twine("failed to convert z/OS member header to EBCDIC: ") +
+                       EC.message());
   Out << EHeader.str();
 }
 
@@ -332,7 +339,7 @@ printMemberHeader(raw_ostream &Out, uint64_t Pos, raw_ostream &StringTable,
     return printBSDMemberHeader(Out, Pos, MemberName, ModTime, M.UID, M.GID,
                                 M.Perms, Size);
   if (isZOSArchive(Kind))
-    return printZOSMemberHeader(Out, Pos, MemberName, ModTime, M.UID, M.GID,
+    return printZOSMemberHeader(Out, MemberName, ModTime, M.UID, M.GID,
                                 M.Perms, Size);
   if (!useStringTable(Thin, MemberName))
     return printGNUSmallMemberHeader(Out, MemberName, ModTime, M.UID, M.GID,
@@ -487,8 +494,7 @@ static void writeSymbolTableHeader(raw_ostream &Out, object::Archive::Kind Kind,
                                 PrevMemberOffset, NextMemberOffset);
   } else if (isZOSArchive(Kind)) {
     const char *Name = "__.SYMDEF";
-    printZOSMemberHeader(Out, Out.tell(), Name, now(Deterministic), 0, 0, 0,
-                         Size);
+    printZOSMemberHeader(Out, Name, now(Deterministic), 0, 0, 0, Size);
   } else {
     const char *Name = is64BitKind(Kind) ? "/SYM64" : "";
     printGNUSmallMemberHeader(Out, Name, now(Deterministic), 0, 0, 0, Size);
@@ -684,7 +690,10 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
     printNBits(Out, Kind, StringTable.size());
   if (isZOSArchive(Kind)) {
     SmallString<256> EStringTable;
-    ConverterEBCDIC::convertToEBCDIC(StringTable, EStringTable);
+    if (std::error_code EC =
+            ConverterEBCDIC::convertToEBCDIC(StringTable, EStringTable))
+      report_fatal_error(Twine("failed to convert z/OS symbol table to EBCDIC: ") +
+                         EC.message());
     Out << EStringTable.str();
   } else {
     Out << StringTable;

>From 4e954480fd409129e6d7d89fd65eb819edc480f0 Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Fri, 10 Jul 2026 16:49:57 -0400
Subject: [PATCH 6/9] Subtract padding prior to writing symbol header, add
 FIXME for symbol flags.

---
 llvm/lib/Object/ArchiveWriter.cpp | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/llvm/lib/Object/ArchiveWriter.cpp b/llvm/lib/Object/ArchiveWriter.cpp
index 283f2bf0fbd07..efa5a2e311f49 100644
--- a/llvm/lib/Object/ArchiveWriter.cpp
+++ b/llvm/lib/Object/ArchiveWriter.cpp
@@ -653,12 +653,15 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
   uint32_t Pad;
   uint64_t Size = computeSymbolTableSize(Kind, NumSyms, OffsetSize,
                                          StringTable.size(), &Pad);
-  writeSymbolTableHeader(Out, Kind, Deterministic, Size, PrevMemberOffset,
-                         NextMemberOffset);
+
   // Padding size is not included in the Size field of the z/OS symbol table
   // header.
+  int64_t HeaderSize = Size;
   if (isZOSArchive(Kind))
-    Size -= Pad;
+    HeaderSize -= Pad;
+
+  writeSymbolTableHeader(Out, Kind, Deterministic, HeaderSize, PrevMemberOffset,
+                         NextMemberOffset);
 
   if (isBSDLike(Kind))
     printNBits(Out, Kind, NumSyms * 2 * OffsetSize);
@@ -679,8 +682,9 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
       if (isBSDLike(Kind))
         printNBits(Out, Kind, StringOffset);
       printNBits(Out, Kind, Pos); // member offset
+      // FIXME: Properly handle symbol attributes for z/OS archives.
       if (isZOSArchive(Kind))
-        printNBits(Out, Kind, Pos); // symbol flags
+        printNBits(Out, Kind, 0); // symbol flags
     }
     Pos += M.Header.size() + M.Data.size() + M.Padding.size();
   }

>From 33951b936c6450a49bdb51e14d99c0deb61d49e2 Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Sun, 12 Jul 2026 23:54:00 -0400
Subject: [PATCH 7/9] Update test case for archive clean up

---
 llvm/test/tools/llvm-ar/zos-write.test | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/llvm/test/tools/llvm-ar/zos-write.test b/llvm/test/tools/llvm-ar/zos-write.test
index d09d1214a328c..a3209a85db296 100644
--- a/llvm/test/tools/llvm-ar/zos-write.test
+++ b/llvm/test/tools/llvm-ar/zos-write.test
@@ -1,6 +1,6 @@
 ## Test writing a z/OS archive.
 
-# RUN: rm -rf %t.dir && mkdir -p %t.dir
+# RUN: rm -rf %t.dir %t.a %t.odd.a %t.long.a %t.sym.a %t.blank-sym.a && mkdir -p %t.dir
 # RUN: split-file %s %t.dir
 
 ## Create a z/OS archive from a plain text file with symbol table writing disabled.

>From 4ffc1f51a0f3f1b379d23cd3f45ef3ad526f5e04 Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Mon, 13 Jul 2026 13:34:03 -0400
Subject: [PATCH 8/9] Update long name handling.

---
 llvm/lib/Object/ArchiveWriter.cpp      | 15 ++++++++++-----
 llvm/test/tools/llvm-ar/zos-write.test |  7 ++++++-
 2 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/llvm/lib/Object/ArchiveWriter.cpp b/llvm/lib/Object/ArchiveWriter.cpp
index efa5a2e311f49..9a8999dcc9250 100644
--- a/llvm/lib/Object/ArchiveWriter.cpp
+++ b/llvm/lib/Object/ArchiveWriter.cpp
@@ -432,7 +432,6 @@ static uint64_t computeSymbolTableSize(object::Archive::Kind Kind,
     Size += NumSyms * OffsetSize * 2; // Table
   else
     Size += NumSyms * OffsetSize; // Table
-
   if (isBSDLike(Kind))
     Size += OffsetSize; // byte count
   Size += StringTableSize;
@@ -924,6 +923,7 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
       if (!SymFileOrErr)
         return createFileError(M.MemberName, SymFileOrErr.takeError());
       D.SymFile = std::move(*SymFileOrErr);
+
       if (SymMap && D.SymFile.get()) {
         auto COFFObj = dyn_cast<COFFObjectFile>(D.SymFile.get());
         std::optional<MemoryBufferRef> HybridView;
@@ -1007,13 +1007,18 @@ computeMemberData(raw_ostream &StringTable, raw_ostream &SymNames,
     // is happy with archives that we generate.
     unsigned MemberPadding =
         isDarwin(Kind) ? offsetToAlignment(D.Data.size(), Align(8)) : 0;
-    unsigned TailPadding =
-        offsetToAlignment(D.Data.size() + MemberPadding, Align(2));
-    D.Padding = StringRef(isZOSArchive(Kind) ? ZOSPaddingData : PaddingData,
-                          MemberPadding + TailPadding);
 
     StringRef MemberName = D.HybridName.size() ? D.HybridName : M->MemberName;
 
+    // z/OS stores long member names inline using their exact byte length.
+    // Include the inline name when computing alignment.
+    uint64_t PaddingBase = D.Data.size() + MemberPadding;
+    if (isZOSArchive(Kind) && MemberName.size() > 16)
+      PaddingBase += MemberName.size();
+    unsigned TailPadding = offsetToAlignment(PaddingBase, Align(2));
+    D.Padding = StringRef(isZOSArchive(Kind) ? ZOSPaddingData : PaddingData,
+                          MemberPadding + TailPadding);
+
     sys::TimePoint<std::chrono::seconds> ModTime;
     if (UniqueTimestamps)
       // Increment timestamp for each file of a given name.
diff --git a/llvm/test/tools/llvm-ar/zos-write.test b/llvm/test/tools/llvm-ar/zos-write.test
index a3209a85db296..0e390183f8d00 100644
--- a/llvm/test/tools/llvm-ar/zos-write.test
+++ b/llvm/test/tools/llvm-ar/zos-write.test
@@ -28,14 +28,19 @@
 
 # PAD: 15
 
-## Long member names should be written and read back correctly.
+## Long member names should use their exact byte length and be read back
+## correctly. The 25-byte name plus the 3-byte contents is already even-sized,
+## so the archive should not contain a trailing padding byte.
 # RUN: printf 'xyz' > %t.dir/very_long_member_name.txt
 # RUN: llvm-ar rcSD --format=zos %t.long.a %t.dir/very_long_member_name.txt
 # RUN: llvm-ar t %t.long.a | FileCheck %s --check-prefix=LONG-LIST
 # RUN: llvm-ar p %t.long.a very_long_member_name.txt | FileCheck %s --check-prefix=LONG-CONTENT
+# RUN: %python -c "d=open(r'%t.long.a','rb').read(); print(d[8:24].decode('cp037').strip()); print(len(d))" | FileCheck %s --check-prefix=LONG-BYTES
 
 # LONG-LIST: very_long_member_name.txt
 # LONG-CONTENT: xyz
+# LONG-BYTES: #1/25
+# LONG-BYTES-NEXT: 96
 
 ## Use LLVM bitcode here so the test remains target-independent. The archive
 ## writer only needs a symbolic input in order to emit the archive symbol table.

>From 27c245b5c993176f854d8b2db2295f9b857f8a51 Mon Sep 17 00:00:00 2001
From: Amy Kwan <amy.kwan1 at ibm.com>
Date: Mon, 13 Jul 2026 21:05:48 +0000
Subject: [PATCH 9/9] Apply clang format.

---
 llvm/lib/Object/ArchiveWriter.cpp | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/llvm/lib/Object/ArchiveWriter.cpp b/llvm/lib/Object/ArchiveWriter.cpp
index 9a8999dcc9250..9af8e0efe1872 100644
--- a/llvm/lib/Object/ArchiveWriter.cpp
+++ b/llvm/lib/Object/ArchiveWriter.cpp
@@ -274,14 +274,14 @@ printZOSMemberHeader(raw_ostream &Out, StringRef Name,
     // z/OS ar stores the exact name length inline with no extra alignment
     // padding, unlike the BSD format which pads to an 8-byte boundary.
     printWithSpacePadding(AOut, Twine("#1/") + Twine(Name.size()), 16);
-    printRestOfMemberHeader(AOut, ModTime, UID, GID, Perms,
-                            Name.size() + Size);
+    printRestOfMemberHeader(AOut, ModTime, UID, GID, Perms, Name.size() + Size);
     AOut << Name;
   }
   SmallString<256> EHeader;
   if (std::error_code EC = ConverterEBCDIC::convertToEBCDIC(AHeader, EHeader))
-    report_fatal_error(Twine("failed to convert z/OS member header to EBCDIC: ") +
-                       EC.message());
+    report_fatal_error(
+        Twine("failed to convert z/OS member header to EBCDIC: ") +
+        EC.message());
   Out << EHeader.str();
 }
 
@@ -339,8 +339,8 @@ printMemberHeader(raw_ostream &Out, uint64_t Pos, raw_ostream &StringTable,
     return printBSDMemberHeader(Out, Pos, MemberName, ModTime, M.UID, M.GID,
                                 M.Perms, Size);
   if (isZOSArchive(Kind))
-    return printZOSMemberHeader(Out, MemberName, ModTime, M.UID, M.GID,
-                                M.Perms, Size);
+    return printZOSMemberHeader(Out, MemberName, ModTime, M.UID, M.GID, M.Perms,
+                                Size);
   if (!useStringTable(Thin, MemberName))
     return printGNUSmallMemberHeader(Out, MemberName, ModTime, M.UID, M.GID,
                                      M.Perms, Size);
@@ -695,8 +695,9 @@ static void writeSymbolTable(raw_ostream &Out, object::Archive::Kind Kind,
     SmallString<256> EStringTable;
     if (std::error_code EC =
             ConverterEBCDIC::convertToEBCDIC(StringTable, EStringTable))
-      report_fatal_error(Twine("failed to convert z/OS symbol table to EBCDIC: ") +
-                         EC.message());
+      report_fatal_error(
+          Twine("failed to convert z/OS symbol table to EBCDIC: ") +
+          EC.message());
     Out << EStringTable.str();
   } else {
     Out << StringTable;



More information about the llvm-commits mailing list