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(__ANDROID__) |
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()).time_since_epoch()).count(); |
115 | } else { |
116 | LLDB_LOG_ERROR(l, exp_mtime.takeError(), |
117 | "failed to get archive object time: {0}" ); |
118 | continue; |
119 | } |
120 | |
121 | auto exp_size = child.getRawSize(); |
122 | if (exp_size) { |
123 | obj.size = exp_size.get(); |
124 | } else { |
125 | LLDB_LOG_ERROR(l, exp_size.takeError(), |
126 | "failed to get archive object size: {0}" ); |
127 | continue; |
128 | } |
129 | |
130 | obj.file_offset = child.getDataOffset(); |
131 | |
132 | auto exp_file_size = child.getSize(); |
133 | if (exp_file_size) { |
134 | obj.file_size = exp_file_size.get(); |
135 | } else { |
136 | LLDB_LOG_ERROR(l, exp_file_size.takeError(), |
137 | "failed to get archive object file size: {0}" ); |
138 | continue; |
139 | } |
140 | m_object_name_to_index_map.Append(unique_cstr: obj.ar_name, value: m_objects.size()); |
141 | m_objects.push_back(x: obj); |
142 | } |
143 | if (iter_err) { |
144 | LLDB_LOG_ERROR(l, std::move(iter_err), |
145 | "failed to iterate over archive objects: {0}" ); |
146 | } |
147 | // Now sort all of the object name pointers |
148 | m_object_name_to_index_map.Sort(); |
149 | return m_objects.size(); |
150 | } |
151 | |
152 | ObjectContainerBSDArchive::Object * |
153 | ObjectContainerBSDArchive::Archive::FindObject( |
154 | ConstString object_name, const llvm::sys::TimePoint<> &object_mod_time) { |
155 | const ObjectNameToIndexMap::Entry *match = |
156 | m_object_name_to_index_map.FindFirstValueForName(unique_cstr: object_name); |
157 | if (!match) |
158 | return nullptr; |
159 | if (object_mod_time == llvm::sys::TimePoint<>()) |
160 | return &m_objects[match->value]; |
161 | |
162 | const uint64_t object_modification_date = llvm::sys::toTimeT(TP: object_mod_time); |
163 | if (m_objects[match->value].modification_time == object_modification_date) |
164 | return &m_objects[match->value]; |
165 | |
166 | const ObjectNameToIndexMap::Entry *next_match = |
167 | m_object_name_to_index_map.FindNextValueForName(entry_ptr: match); |
168 | while (next_match) { |
169 | if (m_objects[next_match->value].modification_time == |
170 | object_modification_date) |
171 | return &m_objects[next_match->value]; |
172 | next_match = m_object_name_to_index_map.FindNextValueForName(entry_ptr: next_match); |
173 | } |
174 | |
175 | return nullptr; |
176 | } |
177 | |
178 | ObjectContainerBSDArchive::Archive::shared_ptr |
179 | ObjectContainerBSDArchive::Archive::FindCachedArchive( |
180 | const FileSpec &file, const ArchSpec &arch, |
181 | const llvm::sys::TimePoint<> &time, lldb::offset_t file_offset) { |
182 | std::lock_guard<std::recursive_mutex> guard(Archive::GetArchiveCacheMutex()); |
183 | shared_ptr archive_sp; |
184 | Archive::Map &archive_map = Archive::GetArchiveCache(); |
185 | Archive::Map::iterator pos = archive_map.find(x: file); |
186 | // Don't cache a value for "archive_map.end()" below since we might delete an |
187 | // archive entry... |
188 | while (pos != archive_map.end() && pos->first == file) { |
189 | bool match = true; |
190 | if (arch.IsValid() && |
191 | !pos->second->GetArchitecture().IsCompatibleMatch(rhs: arch)) |
192 | match = false; |
193 | else if (file_offset != LLDB_INVALID_OFFSET && |
194 | pos->second->GetFileOffset() != file_offset) |
195 | match = false; |
196 | if (match) { |
197 | if (pos->second->GetModificationTime() == time) { |
198 | return pos->second; |
199 | } else { |
200 | // We have a file at the same path with the same architecture whose |
201 | // modification time doesn't match. It doesn't make sense for us to |
202 | // continue to use this BSD archive since we cache only the object info |
203 | // which consists of file time info and also the file offset and file |
204 | // size of any contained objects. Since this information is now out of |
205 | // date, we won't get the correct information if we go and extract the |
206 | // file data, so we should remove the old and outdated entry. |
207 | archive_map.erase(position: pos); |
208 | pos = archive_map.find(x: file); |
209 | continue; // Continue to next iteration so we don't increment pos |
210 | // below... |
211 | } |
212 | } |
213 | ++pos; |
214 | } |
215 | return archive_sp; |
216 | } |
217 | |
218 | ObjectContainerBSDArchive::Archive::shared_ptr |
219 | ObjectContainerBSDArchive::Archive::ParseAndCacheArchiveForFile( |
220 | const FileSpec &file, const ArchSpec &arch, |
221 | const llvm::sys::TimePoint<> &time, lldb::offset_t file_offset, |
222 | DataExtractor &data, ArchiveType archive_type) { |
223 | shared_ptr archive_sp( |
224 | new Archive(arch, time, file_offset, data, archive_type)); |
225 | if (archive_sp) { |
226 | const size_t num_objects = archive_sp->ParseObjects(); |
227 | if (num_objects > 0) { |
228 | std::lock_guard<std::recursive_mutex> guard( |
229 | Archive::GetArchiveCacheMutex()); |
230 | Archive::GetArchiveCache().insert(x: std::make_pair(x: file, y&: archive_sp)); |
231 | } else { |
232 | archive_sp.reset(); |
233 | } |
234 | } |
235 | return archive_sp; |
236 | } |
237 | |
238 | ObjectContainerBSDArchive::Archive::Map & |
239 | ObjectContainerBSDArchive::Archive::GetArchiveCache() { |
240 | static Archive::Map g_archive_map; |
241 | return g_archive_map; |
242 | } |
243 | |
244 | std::recursive_mutex & |
245 | ObjectContainerBSDArchive::Archive::GetArchiveCacheMutex() { |
246 | static std::recursive_mutex g_archive_map_mutex; |
247 | return g_archive_map_mutex; |
248 | } |
249 | |
250 | void ObjectContainerBSDArchive::Initialize() { |
251 | PluginManager::RegisterPlugin(name: GetPluginNameStatic(), |
252 | description: GetPluginDescriptionStatic(), create_callback: CreateInstance, |
253 | get_module_specifications: GetModuleSpecifications); |
254 | } |
255 | |
256 | void ObjectContainerBSDArchive::Terminate() { |
257 | PluginManager::UnregisterPlugin(create_callback: CreateInstance); |
258 | } |
259 | |
260 | ObjectContainer *ObjectContainerBSDArchive::CreateInstance( |
261 | const lldb::ModuleSP &module_sp, DataBufferSP &data_sp, |
262 | lldb::offset_t data_offset, const FileSpec *file, |
263 | lldb::offset_t file_offset, lldb::offset_t length) { |
264 | ConstString object_name(module_sp->GetObjectName()); |
265 | if (!object_name) |
266 | return nullptr; |
267 | |
268 | if (data_sp) { |
269 | // We have data, which means this is the first 512 bytes of the file Check |
270 | // to see if the magic bytes match and if they do, read the entire table of |
271 | // contents for the archive and cache it |
272 | DataExtractor data; |
273 | data.SetData(data_sp, offset: data_offset, length); |
274 | ArchiveType archive_type = ObjectContainerBSDArchive::MagicBytesMatch(data); |
275 | if (file && data_sp && archive_type != ArchiveType::Invalid) { |
276 | LLDB_SCOPED_TIMERF( |
277 | "ObjectContainerBSDArchive::CreateInstance (module = %s, file = " |
278 | "%p, file_offset = 0x%8.8" PRIx64 ", file_size = 0x%8.8" PRIx64 ")" , |
279 | module_sp->GetFileSpec().GetPath().c_str(), |
280 | static_cast<const void *>(file), static_cast<uint64_t>(file_offset), |
281 | static_cast<uint64_t>(length)); |
282 | |
283 | // Map the entire .a file to be sure that we don't lose any data if the |
284 | // file gets updated by a new build while this .a file is being used for |
285 | // debugging |
286 | DataBufferSP archive_data_sp = |
287 | FileSystem::Instance().CreateDataBuffer(file_spec: *file, size: length, offset: file_offset); |
288 | if (!archive_data_sp) |
289 | return nullptr; |
290 | |
291 | lldb::offset_t archive_data_offset = 0; |
292 | |
293 | Archive::shared_ptr archive_sp(Archive::FindCachedArchive( |
294 | file: *file, arch: module_sp->GetArchitecture(), time: module_sp->GetModificationTime(), |
295 | file_offset)); |
296 | std::unique_ptr<ObjectContainerBSDArchive> container_up( |
297 | new ObjectContainerBSDArchive(module_sp, archive_data_sp, |
298 | archive_data_offset, file, file_offset, |
299 | length, archive_type)); |
300 | |
301 | if (container_up) { |
302 | if (archive_sp) { |
303 | // We already have this archive in our cache, use it |
304 | container_up->SetArchive(archive_sp); |
305 | return container_up.release(); |
306 | } else if (container_up->ParseHeader()) |
307 | return container_up.release(); |
308 | } |
309 | } |
310 | } else { |
311 | // No data, just check for a cached archive |
312 | Archive::shared_ptr archive_sp(Archive::FindCachedArchive( |
313 | file: *file, arch: module_sp->GetArchitecture(), time: module_sp->GetModificationTime(), |
314 | file_offset)); |
315 | if (archive_sp) { |
316 | std::unique_ptr<ObjectContainerBSDArchive> container_up( |
317 | new ObjectContainerBSDArchive(module_sp, data_sp, data_offset, file, |
318 | file_offset, length, |
319 | archive_sp->GetArchiveType())); |
320 | |
321 | if (container_up) { |
322 | // We already have this archive in our cache, use it |
323 | container_up->SetArchive(archive_sp); |
324 | return container_up.release(); |
325 | } |
326 | } |
327 | } |
328 | return nullptr; |
329 | } |
330 | |
331 | ArchiveType |
332 | ObjectContainerBSDArchive::(const DataExtractor &data) { |
333 | uint32_t offset = 0; |
334 | const char *armag = (const char *)data.PeekData(offset, |
335 | length: sizeof(ar_hdr) + SARMAG); |
336 | if (armag == nullptr) |
337 | return ArchiveType::Invalid; |
338 | ArchiveType result = ArchiveType::Invalid; |
339 | if (strncmp(s1: armag, s2: ArchiveMagic, SARMAG) == 0) |
340 | result = ArchiveType::Archive; |
341 | else if (strncmp(s1: armag, s2: ThinArchiveMagic, SARMAG) == 0) |
342 | result = ArchiveType::ThinArchive; |
343 | else |
344 | return ArchiveType::Invalid; |
345 | |
346 | armag += offsetof(struct ar_hdr, ar_fmag) + SARMAG; |
347 | if (strncmp(s1: armag, ARFMAG, n: 2) == 0) |
348 | return result; |
349 | return ArchiveType::Invalid; |
350 | } |
351 | |
352 | ObjectContainerBSDArchive::ObjectContainerBSDArchive( |
353 | const lldb::ModuleSP &module_sp, DataBufferSP &data_sp, |
354 | lldb::offset_t data_offset, const lldb_private::FileSpec *file, |
355 | lldb::offset_t file_offset, lldb::offset_t size, ArchiveType archive_type) |
356 | : ObjectContainer(module_sp, file, file_offset, size, data_sp, data_offset), |
357 | m_archive_sp() { |
358 | m_archive_type = archive_type; |
359 | } |
360 | |
361 | void ObjectContainerBSDArchive::SetArchive(Archive::shared_ptr &archive_sp) { |
362 | m_archive_sp = archive_sp; |
363 | } |
364 | |
365 | ObjectContainerBSDArchive::~ObjectContainerBSDArchive() = default; |
366 | |
367 | bool ObjectContainerBSDArchive::() { |
368 | if (m_archive_sp.get() == nullptr) { |
369 | if (m_data.GetByteSize() > 0) { |
370 | ModuleSP module_sp(GetModule()); |
371 | if (module_sp) { |
372 | m_archive_sp = Archive::ParseAndCacheArchiveForFile( |
373 | file: m_file, arch: module_sp->GetArchitecture(), |
374 | time: module_sp->GetModificationTime(), file_offset: m_offset, data&: m_data, archive_type: m_archive_type); |
375 | } |
376 | // Clear the m_data that contains the entire archive data and let our |
377 | // m_archive_sp hold onto the data. |
378 | m_data.Clear(); |
379 | } |
380 | } |
381 | return m_archive_sp.get() != nullptr; |
382 | } |
383 | |
384 | FileSpec GetChildFileSpecificationsFromThin(llvm::StringRef childPath, |
385 | const FileSpec &parentFileSpec) { |
386 | llvm::SmallString<128> FullPath; |
387 | if (llvm::sys::path::is_absolute(path: childPath)) { |
388 | FullPath = childPath; |
389 | } else { |
390 | FullPath = parentFileSpec.GetDirectory().GetStringRef(); |
391 | llvm::sys::path::append(path&: FullPath, a: childPath); |
392 | } |
393 | FileSpec child = FileSpec(FullPath.str(), llvm::sys::path::Style::posix); |
394 | return child; |
395 | } |
396 | |
397 | ObjectFileSP ObjectContainerBSDArchive::GetObjectFile(const FileSpec *file) { |
398 | ModuleSP module_sp(GetModule()); |
399 | if (module_sp) { |
400 | if (module_sp->GetObjectName() && m_archive_sp) { |
401 | Object *object = m_archive_sp->FindObject( |
402 | object_name: module_sp->GetObjectName(), object_mod_time: module_sp->GetObjectModificationTime()); |
403 | if (object) { |
404 | if (m_archive_type == ArchiveType::ThinArchive) { |
405 | // Set file to child object file |
406 | FileSpec child = GetChildFileSpecificationsFromThin( |
407 | childPath: object->ar_name.GetStringRef(), parentFileSpec: m_file); |
408 | lldb::offset_t file_offset = 0; |
409 | lldb::offset_t file_size = object->size; |
410 | std::shared_ptr<DataBuffer> child_data_sp = |
411 | FileSystem::Instance().CreateDataBuffer(file_spec: child, size: file_size, |
412 | offset: file_offset); |
413 | if (!child_data_sp || |
414 | child_data_sp->GetByteSize() != object->file_size) |
415 | return ObjectFileSP(); |
416 | lldb::offset_t data_offset = 0; |
417 | return ObjectFile::FindPlugin( |
418 | module_sp, file_spec: &child, file_offset: m_offset + object->file_offset, |
419 | file_size: object->file_size, data_sp&: child_data_sp, data_offset); |
420 | } |
421 | lldb::offset_t data_offset = object->file_offset; |
422 | return ObjectFile::FindPlugin( |
423 | module_sp, file_spec: file, file_offset: m_offset + object->file_offset, file_size: object->file_size, |
424 | data_sp&: m_archive_sp->GetData().GetSharedDataBuffer(), data_offset); |
425 | } |
426 | } |
427 | } |
428 | return ObjectFileSP(); |
429 | } |
430 | |
431 | size_t ObjectContainerBSDArchive::GetModuleSpecifications( |
432 | const lldb_private::FileSpec &file, lldb::DataBufferSP &data_sp, |
433 | lldb::offset_t data_offset, lldb::offset_t file_offset, |
434 | lldb::offset_t file_size, lldb_private::ModuleSpecList &specs) { |
435 | |
436 | // We have data, which means this is the first 512 bytes of the file Check to |
437 | // see if the magic bytes match and if they do, read the entire table of |
438 | // contents for the archive and cache it |
439 | DataExtractor data; |
440 | data.SetData(data_sp, offset: data_offset, length: data_sp->GetByteSize()); |
441 | ArchiveType archive_type = ObjectContainerBSDArchive::MagicBytesMatch(data); |
442 | if (!file || !data_sp || archive_type == ArchiveType::Invalid) |
443 | return 0; |
444 | |
445 | const size_t initial_count = specs.GetSize(); |
446 | llvm::sys::TimePoint<> file_mod_time = FileSystem::Instance().GetModificationTime(file_spec: file); |
447 | Archive::shared_ptr archive_sp( |
448 | Archive::FindCachedArchive(file, arch: ArchSpec(), time: file_mod_time, file_offset)); |
449 | bool set_archive_arch = false; |
450 | if (!archive_sp) { |
451 | set_archive_arch = true; |
452 | data_sp = |
453 | FileSystem::Instance().CreateDataBuffer(file_spec: file, size: file_size, offset: file_offset); |
454 | if (data_sp) { |
455 | data.SetData(data_sp, offset: 0, length: data_sp->GetByteSize()); |
456 | archive_sp = Archive::ParseAndCacheArchiveForFile( |
457 | file, arch: ArchSpec(), time: file_mod_time, file_offset, data, archive_type); |
458 | } |
459 | } |
460 | |
461 | if (archive_sp) { |
462 | const size_t num_objects = archive_sp->GetNumObjects(); |
463 | for (size_t idx = 0; idx < num_objects; ++idx) { |
464 | const Object *object = archive_sp->GetObjectAtIndex(idx); |
465 | if (object) { |
466 | if (archive_sp->GetArchiveType() == ArchiveType::ThinArchive) { |
467 | if (object->ar_name.IsEmpty()) |
468 | continue; |
469 | FileSpec child = GetChildFileSpecificationsFromThin( |
470 | childPath: object->ar_name.GetStringRef(), parentFileSpec: file); |
471 | if (ObjectFile::GetModuleSpecifications(file: child, file_offset: 0, file_size: object->file_size, |
472 | specs)) { |
473 | ModuleSpec &spec = |
474 | specs.GetModuleSpecRefAtIndex(i: specs.GetSize() - 1); |
475 | llvm::sys::TimePoint<> object_mod_time( |
476 | std::chrono::seconds(object->modification_time)); |
477 | spec.GetObjectName() = object->ar_name; |
478 | spec.SetObjectOffset(0); |
479 | spec.SetObjectSize(object->file_size); |
480 | spec.GetObjectModificationTime() = object_mod_time; |
481 | } |
482 | continue; |
483 | } |
484 | const lldb::offset_t object_file_offset = |
485 | file_offset + object->file_offset; |
486 | if (object->file_offset < file_size && file_size > object_file_offset) { |
487 | if (ObjectFile::GetModuleSpecifications( |
488 | file, file_offset: object_file_offset, file_size: file_size - object_file_offset, |
489 | specs)) { |
490 | ModuleSpec &spec = |
491 | specs.GetModuleSpecRefAtIndex(i: specs.GetSize() - 1); |
492 | llvm::sys::TimePoint<> object_mod_time( |
493 | std::chrono::seconds(object->modification_time)); |
494 | spec.GetObjectName() = object->ar_name; |
495 | spec.SetObjectOffset(object_file_offset); |
496 | spec.SetObjectSize(object->file_size); |
497 | spec.GetObjectModificationTime() = object_mod_time; |
498 | } |
499 | } |
500 | } |
501 | } |
502 | } |
503 | const size_t end_count = specs.GetSize(); |
504 | size_t num_specs_added = end_count - initial_count; |
505 | if (set_archive_arch && num_specs_added > 0) { |
506 | // The archive was created but we didn't have an architecture so we need to |
507 | // set it |
508 | for (size_t i = initial_count; i < end_count; ++i) { |
509 | ModuleSpec module_spec; |
510 | if (specs.GetModuleSpecAtIndex(i, module_spec)) { |
511 | if (module_spec.GetArchitecture().IsValid()) { |
512 | archive_sp->SetArchitecture(module_spec.GetArchitecture()); |
513 | break; |
514 | } |
515 | } |
516 | } |
517 | } |
518 | return num_specs_added; |
519 | } |
520 | |