1 | //===-- ObjectContainerBSDArchive.cpp -------------------------------------===// |
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | |
9 | #include "ObjectContainerBSDArchive.h" |
10 | |
11 | #if defined(_WIN32) || defined(_AIX) |
12 | // Defines from ar, missing on Windows |
13 | #define SARMAG 8 |
14 | #define ARFMAG "`\n" |
15 | |
16 | typedef struct ar_hdr { |
17 | char ar_name[16]; |
18 | char ar_date[12]; |
19 | char ar_uid[6], ar_gid[6]; |
20 | char ar_mode[8]; |
21 | char ar_size[10]; |
22 | char ar_fmag[2]; |
23 | } ar_hdr; |
24 | #else |
25 | #include <ar.h> |
26 | #endif |
27 | |
28 | #include "lldb/Core/Module.h" |
29 | #include "lldb/Core/ModuleSpec.h" |
30 | #include "lldb/Core/PluginManager.h" |
31 | #include "lldb/Host/FileSystem.h" |
32 | #include "lldb/Symbol/ObjectFile.h" |
33 | #include "lldb/Utility/ArchSpec.h" |
34 | #include "lldb/Utility/LLDBLog.h" |
35 | #include "lldb/Utility/Stream.h" |
36 | #include "lldb/Utility/Timer.h" |
37 | |
38 | #include "llvm/Object/Archive.h" |
39 | #include "llvm/Support/MemoryBuffer.h" |
40 | |
41 | using namespace lldb; |
42 | using namespace lldb_private; |
43 | |
44 | using namespace llvm::object; |
45 | |
46 | LLDB_PLUGIN_DEFINE(ObjectContainerBSDArchive) |
47 | |
48 | ObjectContainerBSDArchive::Object::Object() : ar_name() {} |
49 | |
50 | void ObjectContainerBSDArchive::Object::Clear() { |
51 | ar_name.Clear(); |
52 | modification_time = 0; |
53 | size = 0; |
54 | file_offset = 0; |
55 | file_size = 0; |
56 | } |
57 | |
58 | void ObjectContainerBSDArchive::Object::Dump() const { |
59 | printf(format: "name = \"%s\"\n" , ar_name.GetCString()); |
60 | printf(format: "mtime = 0x%8.8" PRIx32 "\n" , modification_time); |
61 | printf(format: "size = 0x%8.8" PRIx32 " (%" PRIu32 ")\n" , size, size); |
62 | printf(format: "file_offset = 0x%16.16" PRIx64 " (%" PRIu64 ")\n" , file_offset, |
63 | file_offset); |
64 | printf(format: "file_size = 0x%16.16" PRIx64 " (%" PRIu64 ")\n\n" , file_size, |
65 | file_size); |
66 | } |
67 | |
68 | ObjectContainerBSDArchive::Archive::Archive(const lldb_private::ArchSpec &arch, |
69 | const llvm::sys::TimePoint<> &time, |
70 | lldb::offset_t file_offset, |
71 | lldb_private::DataExtractor &data, |
72 | ArchiveType archive_type) |
73 | : m_arch(arch), m_modification_time(time), m_file_offset(file_offset), |
74 | m_objects(), m_data(data), m_archive_type(archive_type) {} |
75 | |
76 | Log *l = GetLog(mask: LLDBLog::Object); |
77 | ObjectContainerBSDArchive::Archive::~Archive() = default; |
78 | |
79 | size_t ObjectContainerBSDArchive::Archive::ParseObjects() { |
80 | DataExtractor &data = m_data; |
81 | |
82 | std::unique_ptr<llvm::MemoryBuffer> mem_buffer = |
83 | llvm::MemoryBuffer::getMemBuffer( |
84 | InputData: llvm::StringRef((const char *)data.GetDataStart(), |
85 | data.GetByteSize()), |
86 | BufferName: llvm::StringRef(), |
87 | /*RequiresNullTerminator=*/false); |
88 | |
89 | auto exp_ar = llvm::object::Archive::create(Source: mem_buffer->getMemBufferRef()); |
90 | if (!exp_ar) { |
91 | LLDB_LOG_ERROR(l, exp_ar.takeError(), "failed to create archive: {0}" ); |
92 | return 0; |
93 | } |
94 | auto llvm_archive = std::move(exp_ar.get()); |
95 | |
96 | llvm::Error iter_err = llvm::Error::success(); |
97 | Object obj; |
98 | for (const auto &child : llvm_archive->children(Err&: iter_err)) { |
99 | obj.Clear(); |
100 | auto exp_name = child.getName(); |
101 | if (exp_name) { |
102 | obj.ar_name = ConstString(exp_name.get()); |
103 | } else { |
104 | LLDB_LOG_ERROR(l, exp_name.takeError(), |
105 | "failed to get archive object name: {0}" ); |
106 | continue; |
107 | } |
108 | |
109 | auto exp_mtime = child.getLastModified(); |
110 | if (exp_mtime) { |
111 | obj.modification_time = |
112 | std::chrono::duration_cast<std::chrono::seconds>( |
113 | d: std::chrono::time_point_cast<std::chrono::seconds>( |
114 | t: exp_mtime.get()) |
115 | .time_since_epoch()) |
116 | .count(); |
117 | } else { |
118 | LLDB_LOG_ERROR(l, exp_mtime.takeError(), |
119 | "failed to get archive object time: {0}" ); |
120 | continue; |
121 | } |
122 | |
123 | auto exp_size = child.getRawSize(); |
124 | if (exp_size) { |
125 | obj.size = exp_size.get(); |
126 | } else { |
127 | LLDB_LOG_ERROR(l, exp_size.takeError(), |
128 | "failed to get archive object size: {0}" ); |
129 | continue; |
130 | } |
131 | |
132 | obj.file_offset = child.getDataOffset(); |
133 | |
134 | auto exp_file_size = child.getSize(); |
135 | if (exp_file_size) { |
136 | obj.file_size = exp_file_size.get(); |
137 | } else { |
138 | LLDB_LOG_ERROR(l, exp_file_size.takeError(), |
139 | "failed to get archive object file size: {0}" ); |
140 | continue; |
141 | } |
142 | m_object_name_to_index_map.Append(unique_cstr: obj.ar_name, value: m_objects.size()); |
143 | m_objects.push_back(x: obj); |
144 | } |
145 | if (iter_err) { |
146 | LLDB_LOG_ERROR(l, std::move(iter_err), |
147 | "failed to iterate over archive objects: {0}" ); |
148 | } |
149 | // Now sort all of the object name pointers |
150 | m_object_name_to_index_map.Sort(); |
151 | return m_objects.size(); |
152 | } |
153 | |
154 | ObjectContainerBSDArchive::Object * |
155 | ObjectContainerBSDArchive::Archive::FindObject( |
156 | ConstString object_name, const llvm::sys::TimePoint<> &object_mod_time) { |
157 | const ObjectNameToIndexMap::Entry *match = |
158 | m_object_name_to_index_map.FindFirstValueForName(unique_cstr: object_name); |
159 | if (!match) |
160 | return nullptr; |
161 | if (object_mod_time == llvm::sys::TimePoint<>()) |
162 | return &m_objects[match->value]; |
163 | |
164 | const uint64_t object_modification_date = llvm::sys::toTimeT(TP: object_mod_time); |
165 | if (m_objects[match->value].modification_time == object_modification_date) |
166 | return &m_objects[match->value]; |
167 | |
168 | const ObjectNameToIndexMap::Entry *next_match = |
169 | m_object_name_to_index_map.FindNextValueForName(entry_ptr: match); |
170 | while (next_match) { |
171 | if (m_objects[next_match->value].modification_time == |
172 | object_modification_date) |
173 | return &m_objects[next_match->value]; |
174 | next_match = m_object_name_to_index_map.FindNextValueForName(entry_ptr: next_match); |
175 | } |
176 | |
177 | return nullptr; |
178 | } |
179 | |
180 | ObjectContainerBSDArchive::Archive::shared_ptr |
181 | ObjectContainerBSDArchive::Archive::FindCachedArchive( |
182 | const FileSpec &file, const ArchSpec &arch, |
183 | const llvm::sys::TimePoint<> &time, lldb::offset_t file_offset) { |
184 | std::lock_guard<std::recursive_mutex> guard(Archive::GetArchiveCacheMutex()); |
185 | shared_ptr archive_sp; |
186 | Archive::Map &archive_map = Archive::GetArchiveCache(); |
187 | Archive::Map::iterator pos = archive_map.find(x: file); |
188 | // Don't cache a value for "archive_map.end()" below since we might delete an |
189 | // archive entry... |
190 | while (pos != archive_map.end() && pos->first == file) { |
191 | bool match = true; |
192 | if (arch.IsValid() && |
193 | !pos->second->GetArchitecture().IsCompatibleMatch(rhs: arch)) |
194 | match = false; |
195 | else if (file_offset != LLDB_INVALID_OFFSET && |
196 | pos->second->GetFileOffset() != file_offset) |
197 | match = false; |
198 | if (match) { |
199 | if (pos->second->GetModificationTime() == time) { |
200 | return pos->second; |
201 | } else { |
202 | // We have a file at the same path with the same architecture whose |
203 | // modification time doesn't match. It doesn't make sense for us to |
204 | // continue to use this BSD archive since we cache only the object info |
205 | // which consists of file time info and also the file offset and file |
206 | // size of any contained objects. Since this information is now out of |
207 | // date, we won't get the correct information if we go and extract the |
208 | // file data, so we should remove the old and outdated entry. |
209 | archive_map.erase(position: pos); |
210 | pos = archive_map.find(x: file); |
211 | continue; // Continue to next iteration so we don't increment pos |
212 | // below... |
213 | } |
214 | } |
215 | ++pos; |
216 | } |
217 | return archive_sp; |
218 | } |
219 | |
220 | ObjectContainerBSDArchive::Archive::shared_ptr |
221 | ObjectContainerBSDArchive::Archive::ParseAndCacheArchiveForFile( |
222 | const FileSpec &file, const ArchSpec &arch, |
223 | const llvm::sys::TimePoint<> &time, lldb::offset_t file_offset, |
224 | DataExtractor &data, ArchiveType archive_type) { |
225 | shared_ptr archive_sp( |
226 | new Archive(arch, time, file_offset, data, archive_type)); |
227 | if (archive_sp) { |
228 | const size_t num_objects = archive_sp->ParseObjects(); |
229 | if (num_objects > 0) { |
230 | std::lock_guard<std::recursive_mutex> guard( |
231 | Archive::GetArchiveCacheMutex()); |
232 | Archive::GetArchiveCache().insert(x: std::make_pair(x: file, y&: archive_sp)); |
233 | } else { |
234 | archive_sp.reset(); |
235 | } |
236 | } |
237 | return archive_sp; |
238 | } |
239 | |
240 | ObjectContainerBSDArchive::Archive::Map & |
241 | ObjectContainerBSDArchive::Archive::GetArchiveCache() { |
242 | static Archive::Map g_archive_map; |
243 | return g_archive_map; |
244 | } |
245 | |
246 | std::recursive_mutex & |
247 | ObjectContainerBSDArchive::Archive::GetArchiveCacheMutex() { |
248 | static std::recursive_mutex g_archive_map_mutex; |
249 | return g_archive_map_mutex; |
250 | } |
251 | |
252 | void ObjectContainerBSDArchive::Initialize() { |
253 | PluginManager::RegisterPlugin(name: GetPluginNameStatic(), |
254 | description: GetPluginDescriptionStatic(), create_callback: CreateInstance, |
255 | get_module_specifications: GetModuleSpecifications); |
256 | } |
257 | |
258 | void ObjectContainerBSDArchive::Terminate() { |
259 | PluginManager::UnregisterPlugin(create_callback: CreateInstance); |
260 | } |
261 | |
262 | ObjectContainer *ObjectContainerBSDArchive::CreateInstance( |
263 | const lldb::ModuleSP &module_sp, DataBufferSP &data_sp, |
264 | lldb::offset_t data_offset, const FileSpec *file, |
265 | lldb::offset_t file_offset, lldb::offset_t length) { |
266 | ConstString object_name(module_sp->GetObjectName()); |
267 | if (!object_name) |
268 | return nullptr; |
269 | |
270 | if (data_sp) { |
271 | // We have data, which means this is the first 512 bytes of the file Check |
272 | // to see if the magic bytes match and if they do, read the entire table of |
273 | // contents for the archive and cache it |
274 | DataExtractor data; |
275 | data.SetData(data_sp, offset: data_offset, length); |
276 | ArchiveType archive_type = ObjectContainerBSDArchive::MagicBytesMatch(data); |
277 | if (file && data_sp && archive_type != ArchiveType::Invalid) { |
278 | LLDB_SCOPED_TIMERF( |
279 | "ObjectContainerBSDArchive::CreateInstance (module = %s, file = " |
280 | "%p, file_offset = 0x%8.8" PRIx64 ", file_size = 0x%8.8" PRIx64 ")" , |
281 | module_sp->GetFileSpec().GetPath().c_str(), |
282 | static_cast<const void *>(file), static_cast<uint64_t>(file_offset), |
283 | static_cast<uint64_t>(length)); |
284 | |
285 | // Map the entire .a file to be sure that we don't lose any data if the |
286 | // file gets updated by a new build while this .a file is being used for |
287 | // debugging |
288 | DataBufferSP archive_data_sp = |
289 | FileSystem::Instance().CreateDataBuffer(file_spec: *file, size: length, offset: file_offset); |
290 | if (!archive_data_sp) |
291 | return nullptr; |
292 | |
293 | lldb::offset_t archive_data_offset = 0; |
294 | |
295 | Archive::shared_ptr archive_sp(Archive::FindCachedArchive( |
296 | file: *file, arch: module_sp->GetArchitecture(), time: module_sp->GetModificationTime(), |
297 | file_offset)); |
298 | std::unique_ptr<ObjectContainerBSDArchive> container_up( |
299 | new ObjectContainerBSDArchive(module_sp, archive_data_sp, |
300 | archive_data_offset, file, file_offset, |
301 | length, archive_type)); |
302 | |
303 | if (container_up) { |
304 | if (archive_sp) { |
305 | // We already have this archive in our cache, use it |
306 | container_up->SetArchive(archive_sp); |
307 | return container_up.release(); |
308 | } else if (container_up->ParseHeader()) |
309 | return container_up.release(); |
310 | } |
311 | } |
312 | } else { |
313 | // No data, just check for a cached archive |
314 | Archive::shared_ptr archive_sp(Archive::FindCachedArchive( |
315 | file: *file, arch: module_sp->GetArchitecture(), time: module_sp->GetModificationTime(), |
316 | file_offset)); |
317 | if (archive_sp) { |
318 | std::unique_ptr<ObjectContainerBSDArchive> container_up( |
319 | new ObjectContainerBSDArchive(module_sp, data_sp, data_offset, file, |
320 | file_offset, length, |
321 | archive_sp->GetArchiveType())); |
322 | |
323 | if (container_up) { |
324 | // We already have this archive in our cache, use it |
325 | container_up->SetArchive(archive_sp); |
326 | return container_up.release(); |
327 | } |
328 | } |
329 | } |
330 | return nullptr; |
331 | } |
332 | |
333 | ArchiveType |
334 | ObjectContainerBSDArchive::(const DataExtractor &data) { |
335 | uint32_t offset = 0; |
336 | const char *armag = |
337 | (const char *)data.PeekData(offset, length: sizeof(ar_hdr) + SARMAG); |
338 | if (armag == nullptr) |
339 | return ArchiveType::Invalid; |
340 | ArchiveType result = ArchiveType::Invalid; |
341 | if (strncmp(s1: armag, s2: ArchiveMagic, SARMAG) == 0) |
342 | result = ArchiveType::Archive; |
343 | else if (strncmp(s1: armag, s2: ThinArchiveMagic, SARMAG) == 0) |
344 | result = ArchiveType::ThinArchive; |
345 | else |
346 | return ArchiveType::Invalid; |
347 | |
348 | armag += offsetof(struct ar_hdr, ar_fmag) + SARMAG; |
349 | if (strncmp(s1: armag, ARFMAG, n: 2) == 0) |
350 | return result; |
351 | return ArchiveType::Invalid; |
352 | } |
353 | |
354 | ObjectContainerBSDArchive::ObjectContainerBSDArchive( |
355 | const lldb::ModuleSP &module_sp, DataBufferSP &data_sp, |
356 | lldb::offset_t data_offset, const lldb_private::FileSpec *file, |
357 | lldb::offset_t file_offset, lldb::offset_t size, ArchiveType archive_type) |
358 | : ObjectContainer(module_sp, file, file_offset, size, data_sp, data_offset), |
359 | m_archive_sp() { |
360 | m_archive_type = archive_type; |
361 | } |
362 | |
363 | void ObjectContainerBSDArchive::SetArchive(Archive::shared_ptr &archive_sp) { |
364 | m_archive_sp = archive_sp; |
365 | } |
366 | |
367 | ObjectContainerBSDArchive::~ObjectContainerBSDArchive() = default; |
368 | |
369 | bool ObjectContainerBSDArchive::() { |
370 | if (m_archive_sp.get() == nullptr) { |
371 | if (m_data.GetByteSize() > 0) { |
372 | ModuleSP module_sp(GetModule()); |
373 | if (module_sp) { |
374 | m_archive_sp = Archive::ParseAndCacheArchiveForFile( |
375 | file: m_file, arch: module_sp->GetArchitecture(), |
376 | time: module_sp->GetModificationTime(), file_offset: m_offset, data&: m_data, archive_type: m_archive_type); |
377 | } |
378 | // Clear the m_data that contains the entire archive data and let our |
379 | // m_archive_sp hold onto the data. |
380 | m_data.Clear(); |
381 | } |
382 | } |
383 | return m_archive_sp.get() != nullptr; |
384 | } |
385 | |
386 | FileSpec GetChildFileSpecificationsFromThin(llvm::StringRef childPath, |
387 | const FileSpec &parentFileSpec) { |
388 | llvm::SmallString<128> FullPath; |
389 | if (llvm::sys::path::is_absolute(path: childPath)) { |
390 | FullPath = childPath; |
391 | } else { |
392 | FullPath = parentFileSpec.GetDirectory().GetStringRef(); |
393 | llvm::sys::path::append(path&: FullPath, a: childPath); |
394 | } |
395 | FileSpec child = FileSpec(FullPath.str(), llvm::sys::path::Style::posix); |
396 | return child; |
397 | } |
398 | |
399 | ObjectFileSP ObjectContainerBSDArchive::GetObjectFile(const FileSpec *file) { |
400 | ModuleSP module_sp(GetModule()); |
401 | if (module_sp) { |
402 | if (module_sp->GetObjectName() && m_archive_sp) { |
403 | Object *object = m_archive_sp->FindObject( |
404 | object_name: module_sp->GetObjectName(), object_mod_time: module_sp->GetObjectModificationTime()); |
405 | if (object) { |
406 | if (m_archive_type == ArchiveType::ThinArchive) { |
407 | // Set file to child object file |
408 | FileSpec child = GetChildFileSpecificationsFromThin( |
409 | childPath: object->ar_name.GetStringRef(), parentFileSpec: m_file); |
410 | lldb::offset_t file_offset = 0; |
411 | lldb::offset_t file_size = object->size; |
412 | std::shared_ptr<DataBuffer> child_data_sp = |
413 | FileSystem::Instance().CreateDataBuffer(file_spec: child, size: file_size, |
414 | offset: file_offset); |
415 | if (!child_data_sp || |
416 | child_data_sp->GetByteSize() != object->file_size) |
417 | return ObjectFileSP(); |
418 | lldb::offset_t data_offset = 0; |
419 | return ObjectFile::FindPlugin( |
420 | module_sp, file_spec: &child, file_offset: m_offset + object->file_offset, |
421 | file_size: object->file_size, data_sp&: child_data_sp, data_offset); |
422 | } |
423 | lldb::offset_t data_offset = object->file_offset; |
424 | return ObjectFile::FindPlugin( |
425 | module_sp, file_spec: file, file_offset: m_offset + object->file_offset, file_size: object->file_size, |
426 | data_sp&: m_archive_sp->GetData().GetSharedDataBuffer(), data_offset); |
427 | } |
428 | } |
429 | } |
430 | return ObjectFileSP(); |
431 | } |
432 | |
433 | size_t ObjectContainerBSDArchive::GetModuleSpecifications( |
434 | const lldb_private::FileSpec &file, lldb::DataBufferSP &data_sp, |
435 | lldb::offset_t data_offset, lldb::offset_t file_offset, |
436 | lldb::offset_t file_size, lldb_private::ModuleSpecList &specs) { |
437 | |
438 | // We have data, which means this is the first 512 bytes of the file Check to |
439 | // see if the magic bytes match and if they do, read the entire table of |
440 | // contents for the archive and cache it |
441 | DataExtractor data; |
442 | data.SetData(data_sp, offset: data_offset, length: data_sp->GetByteSize()); |
443 | ArchiveType archive_type = ObjectContainerBSDArchive::MagicBytesMatch(data); |
444 | if (!file || !data_sp || archive_type == ArchiveType::Invalid) |
445 | return 0; |
446 | |
447 | const size_t initial_count = specs.GetSize(); |
448 | llvm::sys::TimePoint<> file_mod_time = |
449 | FileSystem::Instance().GetModificationTime(file_spec: file); |
450 | Archive::shared_ptr archive_sp( |
451 | Archive::FindCachedArchive(file, arch: ArchSpec(), time: file_mod_time, file_offset)); |
452 | bool set_archive_arch = false; |
453 | if (!archive_sp) { |
454 | set_archive_arch = true; |
455 | data_sp = |
456 | FileSystem::Instance().CreateDataBuffer(file_spec: file, size: file_size, offset: file_offset); |
457 | if (data_sp) { |
458 | data.SetData(data_sp, offset: 0, length: data_sp->GetByteSize()); |
459 | archive_sp = Archive::ParseAndCacheArchiveForFile( |
460 | file, arch: ArchSpec(), time: file_mod_time, file_offset, data, archive_type); |
461 | } |
462 | } |
463 | |
464 | if (archive_sp) { |
465 | const size_t num_objects = archive_sp->GetNumObjects(); |
466 | for (size_t idx = 0; idx < num_objects; ++idx) { |
467 | const Object *object = archive_sp->GetObjectAtIndex(idx); |
468 | if (object) { |
469 | if (archive_sp->GetArchiveType() == ArchiveType::ThinArchive) { |
470 | if (object->ar_name.IsEmpty()) |
471 | continue; |
472 | FileSpec child = GetChildFileSpecificationsFromThin( |
473 | childPath: object->ar_name.GetStringRef(), parentFileSpec: file); |
474 | if (ObjectFile::GetModuleSpecifications(file: child, file_offset: 0, file_size: object->file_size, |
475 | specs)) { |
476 | ModuleSpec &spec = |
477 | specs.GetModuleSpecRefAtIndex(i: specs.GetSize() - 1); |
478 | llvm::sys::TimePoint<> object_mod_time( |
479 | std::chrono::seconds(object->modification_time)); |
480 | spec.GetObjectName() = object->ar_name; |
481 | spec.SetObjectOffset(0); |
482 | spec.SetObjectSize(object->file_size); |
483 | spec.GetObjectModificationTime() = object_mod_time; |
484 | } |
485 | continue; |
486 | } |
487 | const lldb::offset_t object_file_offset = |
488 | file_offset + object->file_offset; |
489 | if (object->file_offset < file_size && file_size > object_file_offset) { |
490 | if (ObjectFile::GetModuleSpecifications( |
491 | file, file_offset: object_file_offset, file_size: file_size - object_file_offset, |
492 | specs)) { |
493 | ModuleSpec &spec = |
494 | specs.GetModuleSpecRefAtIndex(i: specs.GetSize() - 1); |
495 | llvm::sys::TimePoint<> object_mod_time( |
496 | std::chrono::seconds(object->modification_time)); |
497 | spec.GetObjectName() = object->ar_name; |
498 | spec.SetObjectOffset(object_file_offset); |
499 | spec.SetObjectSize(object->file_size); |
500 | spec.GetObjectModificationTime() = object_mod_time; |
501 | } |
502 | } |
503 | } |
504 | } |
505 | } |
506 | const size_t end_count = specs.GetSize(); |
507 | size_t num_specs_added = end_count - initial_count; |
508 | if (set_archive_arch && num_specs_added > 0) { |
509 | // The archive was created but we didn't have an architecture so we need to |
510 | // set it |
511 | for (size_t i = initial_count; i < end_count; ++i) { |
512 | ModuleSpec module_spec; |
513 | if (specs.GetModuleSpecAtIndex(i, module_spec)) { |
514 | if (module_spec.GetArchitecture().IsValid()) { |
515 | archive_sp->SetArchitecture(module_spec.GetArchitecture()); |
516 | break; |
517 | } |
518 | } |
519 | } |
520 | } |
521 | return num_specs_added; |
522 | } |
523 | |