1 |
+ |
#____________________________________________________________________ |
2 |
+ |
# File: ToolManager.pm |
3 |
+ |
#____________________________________________________________________ |
4 |
+ |
# |
5 |
+ |
# Author: Shaun Ashby <Shaun.Ashby@cern.ch> |
6 |
+ |
# Update: 2003-11-12 15:04:16+0100 |
7 |
+ |
# Revision: $Id$ |
8 |
+ |
# |
9 |
+ |
# Copyright: 2003 (C) Shaun Ashby |
10 |
+ |
# |
11 |
+ |
#-------------------------------------------------------------------- |
12 |
+ |
package BuildSystem::ToolManager; |
13 |
+ |
require 5.004; |
14 |
+ |
|
15 |
+ |
use Exporter; |
16 |
+ |
use BuildSystem::ToolCache; |
17 |
+ |
use BuildSystem::ToolParser; |
18 |
+ |
use Utilities::AddDir; |
19 |
+ |
use URL::URLhandler; |
20 |
+ |
use Utilities::Verbose; |
21 |
+ |
|
22 |
+ |
@ISA=qw(BuildSystem::ToolCache Utilities::Verbose); |
23 |
+ |
@EXPORT_OK=qw( ); |
24 |
+ |
# |
25 |
+ |
# |
26 |
+ |
sub new |
27 |
+ |
############################################################### |
28 |
+ |
# new # |
29 |
+ |
############################################################### |
30 |
+ |
# modified : Wed Nov 12 10:34:10 2003 / SFA # |
31 |
+ |
# params : # |
32 |
+ |
# : # |
33 |
+ |
# function : # |
34 |
+ |
# : # |
35 |
+ |
############################################################### |
36 |
+ |
{ |
37 |
+ |
my $proto=shift; |
38 |
+ |
my $class=ref($proto) || $proto; |
39 |
+ |
my $self=$class->SUPER::new(); # Inherit from ToolCache |
40 |
+ |
my $projectarea=shift; |
41 |
+ |
|
42 |
+ |
bless $self,$class; |
43 |
+ |
|
44 |
+ |
$self->{arch}=shift; |
45 |
+ |
$self->{topdir}=$projectarea->location(); |
46 |
+ |
$self->{configdir}=$self->{topdir}."/".$projectarea->configurationdir(); |
47 |
+ |
$self->{cache}=$projectarea->cache(); # Download tool cache |
48 |
+ |
$self->{toolfiledir}=$self->{topdir}."/.SCRAM/InstalledTools"; |
49 |
+ |
$self->{datastore}=$self->{topdir}."/.SCRAM"; |
50 |
+ |
$self->{archstore}=$self->{topdir}."/.SCRAM/".$ENV{SCRAM_ARCH}; |
51 |
+ |
|
52 |
+ |
# Make sure our tool download dir exists: |
53 |
+ |
AddDir::adddir($self->{toolfiledir}); |
54 |
+ |
AddDir::adddir($self->{archstore}); |
55 |
+ |
|
56 |
+ |
# Set the tool cache file to read/write: |
57 |
+ |
$self->name($projectarea->toolcachename()); |
58 |
+ |
|
59 |
+ |
# Check for the downloaded tools cache: |
60 |
+ |
if (exists($self->{cache})) |
61 |
+ |
{ |
62 |
+ |
$self->{urlhandler}=URL::URLhandler->new($self->{cache}); |
63 |
+ |
} |
64 |
+ |
|
65 |
+ |
return $self; |
66 |
+ |
} |
67 |
+ |
|
68 |
+ |
sub clone() |
69 |
+ |
{ |
70 |
+ |
my $self=shift; |
71 |
+ |
my $projectarea=shift; |
72 |
+ |
|
73 |
+ |
# Change cache settings to reflect the new location: |
74 |
+ |
$self->{topdir}=$projectarea->location(); |
75 |
+ |
|
76 |
+ |
$self->{configdir}=$self->{topdir}."/".$projectarea->configurationdir(); |
77 |
+ |
$self->{toolfiledir}=$self->{topdir}."/.SCRAM/InstalledTools"; |
78 |
+ |
$self->{datastore}=$self->{topdir}."/.SCRAM"; |
79 |
+ |
$self->{archstore}=$self->{topdir}."/.SCRAM/".$ENV{SCRAM_ARCH}; |
80 |
+ |
|
81 |
+ |
# Change the cache name: |
82 |
+ |
$self->name($projectarea->toolcachename()); |
83 |
+ |
$self->cloned_tm(1); |
84 |
+ |
|
85 |
+ |
return $self; |
86 |
+ |
} |
87 |
+ |
|
88 |
+ |
sub arch_change_after_copy() |
89 |
+ |
{ |
90 |
+ |
my $self=shift; |
91 |
+ |
my ($newarch, $cachename)=@_; |
92 |
+ |
# Make changes to arch-specific settings when copying tool manager |
93 |
+ |
# object to another arch during setup: |
94 |
+ |
$self->{arch} = $newarch; |
95 |
+ |
$self->{archstore} = $self->{topdir}."/.SCRAM/".$newarch; |
96 |
+ |
# Change the name of the cache to reflect new (arch-specific) location: |
97 |
+ |
$self->name($cachename); |
98 |
+ |
} |
99 |
+ |
|
100 |
+ |
sub interactive() |
101 |
+ |
{ |
102 |
+ |
my $self=shift; |
103 |
+ |
# Interactive mode on/off: |
104 |
+ |
@_ ? $self->{interactive} = shift |
105 |
+ |
: ((defined $self->{interactive}) ? $self->{interactive} : 0); |
106 |
+ |
} |
107 |
+ |
|
108 |
+ |
sub setupalltools() |
109 |
+ |
{ |
110 |
+ |
my $self = shift; |
111 |
+ |
my ($arealocation,$setupopt) = @_; |
112 |
+ |
my (@localtools); |
113 |
+ |
my $selected; |
114 |
+ |
|
115 |
+ |
# Get the selected tool list. Handle the case where there might not be |
116 |
+ |
# any selected tools: //FIXME: need to handle case where there are no |
117 |
+ |
# selected tools (not very often but a possibility): |
118 |
+ |
my $sel = $self->selected(); |
119 |
+ |
|
120 |
+ |
if (defined ($sel)) |
121 |
+ |
{ |
122 |
+ |
$selected = [ keys %{$sel} ]; |
123 |
+ |
} |
124 |
+ |
|
125 |
+ |
# Setup option "setupopt" directs the setup: 1 is for booting from |
126 |
+ |
# scratch, 0 is when just doing "scram setup" (in this case we don't |
127 |
+ |
# want to pick up everything from any scram-managed projects): |
128 |
+ |
if ($setupopt == 1) # We're booting from scratch |
129 |
+ |
{ |
130 |
+ |
# Check to see if there are any SCRAM-managed projects in our local requirements: |
131 |
+ |
my $scramprojects = $::scram->_loadscramdb(); |
132 |
+ |
|
133 |
+ |
# Look for a match in the scram db: |
134 |
+ |
foreach my $S (@$selected) |
135 |
+ |
{ |
136 |
+ |
if (exists ($scramprojects->{$S})) |
137 |
+ |
{ |
138 |
+ |
# Now check the version required exists in |
139 |
+ |
# list of scram projects with this name: |
140 |
+ |
while (my ($pdata,$plocation) = each %{$scramprojects->{$S}}) |
141 |
+ |
{ |
142 |
+ |
# Split the $pdata string to get the real name and the version: |
143 |
+ |
my ($pname,$pversion) = split(":",$pdata); |
144 |
+ |
if ($pversion eq $self->defaultversion($S)) |
145 |
+ |
{ |
146 |
+ |
# Get the tool manager for the scram project: |
147 |
+ |
my $sa=$::scram->scramfunctions()->scramprojectdb()->getarea($pname,$pversion); |
148 |
+ |
# Load the tool cache: |
149 |
+ |
if ( -r $sa->toolcachename()) |
150 |
+ |
{ |
151 |
+ |
use Cache::CacheUtilities; |
152 |
+ |
my $satoolmanager=&Cache::CacheUtilities::read($sa->toolcachename()); |
153 |
+ |
# Copy needed content from toolmanager for scram-managed project: |
154 |
+ |
$self->inheritcontent($satoolmanager); |
155 |
+ |
} |
156 |
+ |
} |
157 |
+ |
} |
158 |
+ |
# Also add this scram-managed project to list of tools to set up: |
159 |
+ |
push(@localtools,$S); |
160 |
+ |
} |
161 |
+ |
else |
162 |
+ |
{ |
163 |
+ |
# Store other tools in ReqDoc in separate array. We will set up these tools later: |
164 |
+ |
push(@localtools,$S); |
165 |
+ |
} |
166 |
+ |
} |
167 |
+ |
|
168 |
+ |
# Set up extra tools required in this project, in addition to |
169 |
+ |
# any scram-managed projects |
170 |
+ |
foreach my $localtool (@localtools) |
171 |
+ |
{ |
172 |
+ |
# First check to see if it's already set up (i.e., was contained |
173 |
+ |
# in list of requirements for scram project): |
174 |
+ |
if (! $self->definedtool($localtool)) |
175 |
+ |
{ |
176 |
+ |
$self->toolsetup($arealocation,$localtool,$self->defaultversion($localtool)); |
177 |
+ |
$self->addtoselected($localtool); |
178 |
+ |
} |
179 |
+ |
else |
180 |
+ |
{ |
181 |
+ |
print $localtool," already set up.","\n",if ($ENV{SCRAM_DEBUG}); |
182 |
+ |
} |
183 |
+ |
} |
184 |
+ |
} |
185 |
+ |
else |
186 |
+ |
{ |
187 |
+ |
# Just loop over all tools and setup again: |
188 |
+ |
foreach my $localtool (@{$selected}) |
189 |
+ |
{ |
190 |
+ |
$self->toolsetup($arealocation,$localtool,$self->defaultversion($localtool)); |
191 |
+ |
} |
192 |
+ |
} |
193 |
+ |
|
194 |
+ |
print "\n"; |
195 |
+ |
} |
196 |
+ |
|
197 |
+ |
sub coresetup() |
198 |
+ |
{ |
199 |
+ |
my $self=shift; |
200 |
+ |
my ($toolname, $toolversion, $toolfile) = @_; |
201 |
+ |
my ($toolcheck, $toolparser); |
202 |
+ |
|
203 |
+ |
print "\n"; |
204 |
+ |
print $::bold."Setting up ",$toolname," version ",$toolversion,": ".$::normal,"\n"; |
205 |
+ |
|
206 |
+ |
# New ToolParser object for this tool if there isn't one already. |
207 |
+ |
# Look in array of raw tools to see if this tool has a ToolParser object: |
208 |
+ |
$toolcheck=0; |
209 |
+ |
|
210 |
+ |
map |
211 |
+ |
{ |
212 |
+ |
if ($_->toolname() eq $toolname) {$toolcheck = 1; $toolparser = $_;} |
213 |
+ |
} $self->rawtools(); |
214 |
+ |
|
215 |
+ |
# Tool not known so we create a new ToolParser object and parse it: |
216 |
+ |
if ($toolcheck != 1) |
217 |
+ |
{ |
218 |
+ |
$toolparser = BuildSystem::ToolParser->new(); |
219 |
+ |
# We only want to store the stuff relevant for one particular version: |
220 |
+ |
$toolparser->parse($toolname, $toolversion, $toolfile); |
221 |
+ |
# Store the ToolParser object in the cache: |
222 |
+ |
$self->store($toolparser); |
223 |
+ |
} |
224 |
+ |
|
225 |
+ |
# Next, set up the tool: |
226 |
+ |
my $store = $toolparser->processrawtool($self->interactive()); |
227 |
+ |
# Make sure that we have this tool in the list of selected tools (just in case this tool was |
228 |
+ |
# set up by hand afterwards): |
229 |
+ |
$self->addtoselected($toolname); |
230 |
+ |
# Store the ToolData object in the cache: |
231 |
+ |
$self->storeincache($toolparser->toolname(),$store); |
232 |
+ |
return $self; |
233 |
+ |
} |
234 |
+ |
|
235 |
+ |
sub toolsetup() |
236 |
+ |
{ |
237 |
+ |
my $self=shift; |
238 |
+ |
my ($arealocation, $toolname, $toolversion, $toolurl) = @_; |
239 |
+ |
my ($urlcache, $url, $filename, $tfname); |
240 |
+ |
my $toolfile; |
241 |
+ |
|
242 |
+ |
$toolname =~ tr[A-Z][a-z]; |
243 |
+ |
$toolversion ||= $self->defaultversion($toolname); |
244 |
+ |
$urlcache=URL::URLcache->new($arealocation."/.SCRAM/cache"); # Download tool cache |
245 |
+ |
|
246 |
+ |
# Check for the downloaded tools cache: |
247 |
+ |
if (defined($urlcache)) |
248 |
+ |
{ |
249 |
+ |
$self->{urlhandler}=URL::URLhandler->new($urlcache); |
250 |
+ |
} |
251 |
+ |
|
252 |
+ |
$url = $self->toolurls()->{$toolname}; |
253 |
+ |
$filename = $self->{toolfiledir}."/".$toolname; |
254 |
+ |
|
255 |
+ |
# First, check to see if there was a tool URL given. If so, we might need to read |
256 |
+ |
# from http or from a file: type URL: |
257 |
+ |
if (my ($proto, $urlv) = ($toolurl =~ /(.*):(.*)/)) |
258 |
+ |
{ |
259 |
+ |
# See what kind of URL (file:, http:, cvs:, svn:, .. ): |
260 |
+ |
if ($proto eq 'file') |
261 |
+ |
{ |
262 |
+ |
# If the tool url is a file and the file exists, |
263 |
+ |
# copy it to .SCRAM/InstalledTools and set the |
264 |
+ |
# filename accordingly: |
265 |
+ |
if ( -f $urlv) |
266 |
+ |
{ |
267 |
+ |
use File::Copy; |
268 |
+ |
copy($urlv, $filename); |
269 |
+ |
$toolfile=$filename; |
270 |
+ |
} |
271 |
+ |
else |
272 |
+ |
{ |
273 |
+ |
$::scram->scramerror("Unable to set up $toolname from URL $toolurl-- $urlv does not exist!"); |
274 |
+ |
} |
275 |
+ |
} |
276 |
+ |
elsif ($proto eq 'http') |
277 |
+ |
{ |
278 |
+ |
print "SCRAM: downloading $toolname from $toolurl","\n"; |
279 |
+ |
# Download from WWW first: |
280 |
+ |
use LWP::Simple qw(&getstore); |
281 |
+ |
my $http_response_val = &getstore($toolurl, $filename); |
282 |
+ |
|
283 |
+ |
# Check the HTTP status. If doc not found, exit: |
284 |
+ |
if ($http_response_val != 200) |
285 |
+ |
{ |
286 |
+ |
my ($server,$doc) = ($urlv =~ m|//(.*?)/(.*)|); |
287 |
+ |
$::scram->scramerror("Unable to set up $toolname: $doc not found on $server!"); |
288 |
+ |
} |
289 |
+ |
else |
290 |
+ |
{ |
291 |
+ |
$toolfile=$filename; |
292 |
+ |
} |
293 |
+ |
} |
294 |
+ |
elsif ($proto eq 'cvs') |
295 |
+ |
{ |
296 |
+ |
print "SCRAM: downloading $toolname from $urlv using protocol $proto.","\n"; |
297 |
+ |
print "[ not yet supported ]","\n"; |
298 |
+ |
exit(0); |
299 |
+ |
} |
300 |
+ |
elsif ($proto eq 'svn') |
301 |
+ |
{ |
302 |
+ |
print "SCRAM: downloading $toolname from $urlv using protocol $proto.","\n"; |
303 |
+ |
print "[ not yet supported ]","\n"; |
304 |
+ |
exit(0); |
305 |
+ |
} |
306 |
+ |
else |
307 |
+ |
{ |
308 |
+ |
$::scram->scramerror("Unable to download $urlv! Unknown protocol \"$proto\". Bye."); |
309 |
+ |
} |
310 |
+ |
} |
311 |
+ |
else |
312 |
+ |
{ |
313 |
+ |
# Copy the downloaded tool file to InstalledTools directory: |
314 |
+ |
if ( ! -f $filename ) |
315 |
+ |
{ |
316 |
+ |
$self->verbose("Attempting Download of $url"); |
317 |
+ |
# Get file from download cache: |
318 |
+ |
($url,$filename)=$self->{urlhandler}->get($url); |
319 |
+ |
use File::Copy; |
320 |
+ |
$tfname=$self->{toolfiledir}."/".$toolname; |
321 |
+ |
copy($filename, $tfname); |
322 |
+ |
$toolfile=$tfname; |
323 |
+ |
} |
324 |
+ |
else |
325 |
+ |
{ |
326 |
+ |
# File already exists in the .SCRAM/InstallTools directory: |
327 |
+ |
$toolfile=$filename; |
328 |
+ |
} |
329 |
+ |
} |
330 |
+ |
|
331 |
+ |
# Run the core setup routine: |
332 |
+ |
$self->coresetup($toolname, $toolversion, $toolfile); |
333 |
+ |
return $self; |
334 |
+ |
} |
335 |
+ |
|
336 |
+ |
sub setupself() |
337 |
+ |
{ |
338 |
+ |
my $self=shift; |
339 |
+ |
my ($location)=@_; |
340 |
+ |
# Process the file "Self" in local config directory. This is used to |
341 |
+ |
# set all the paths/runtime settings for this project: |
342 |
+ |
my $filename=$location."/config/Self"; |
343 |
+ |
|
344 |
+ |
if ( -f $filename ) |
345 |
+ |
{ |
346 |
+ |
print "\n"; |
347 |
+ |
print $::bold."Setting up SELF:".$::normal,"\n"; |
348 |
+ |
# Self file exists so process it: |
349 |
+ |
$selfparser = BuildSystem::ToolParser->new(); |
350 |
+ |
$selfparser->parse('self','SELF',$filename); |
351 |
+ |
|
352 |
+ |
# Next, set up the tool: |
353 |
+ |
$store = $selfparser->processrawtool($self->interactive()); |
354 |
+ |
|
355 |
+ |
# If we are in a developer area, also add RELEASETOP paths: |
356 |
+ |
if (exists($ENV{RELEASETOP})) |
357 |
+ |
{ |
358 |
+ |
print "\nAdding RELEASE area settings to self....OK","\n", if ($ENV{SCRAM_DEBUG}); |
359 |
+ |
$store->addreleasetoself(); |
360 |
+ |
} |
361 |
+ |
|
362 |
+ |
# Store the ToolData object in the cache: |
363 |
+ |
$self->storeincache($selfparser->toolname(),$store); |
364 |
+ |
print "\n"; |
365 |
+ |
} |
366 |
+ |
else |
367 |
+ |
{ |
368 |
+ |
print "\n"; |
369 |
+ |
print "SCRAM: No file config/Self...nothing to do."; |
370 |
+ |
print "\n"; |
371 |
+ |
return; |
372 |
+ |
} |
373 |
+ |
} |
374 |
+ |
|
375 |
+ |
sub defaultversion() |
376 |
+ |
{ |
377 |
+ |
my $self = shift; |
378 |
+ |
my ($tool) = @_; |
379 |
+ |
# Return default versions as taken from configuration: |
380 |
+ |
return (%{$self->defaultversions()}->{$tool}); |
381 |
+ |
} |
382 |
+ |
|
383 |
+ |
sub storeincache() |
384 |
+ |
{ |
385 |
+ |
my $self=shift; |
386 |
+ |
my ($toolname,$dataobject)=@_; |
387 |
+ |
|
388 |
+ |
# Store ToolData object (for a set-up tool) in cache: |
389 |
+ |
if (ref($dataobject) eq 'BuildSystem::ToolData') |
390 |
+ |
{ |
391 |
+ |
$self->{SETUP}->{$toolname} = $dataobject; |
392 |
+ |
} |
393 |
+ |
else |
394 |
+ |
{ |
395 |
+ |
$::scram->scramerror("ToolManager: BuildSystem::ToolData object reference expected.") |
396 |
+ |
} |
397 |
+ |
} |
398 |
+ |
|
399 |
+ |
sub tools() |
400 |
+ |
{ |
401 |
+ |
my $self = shift; |
402 |
+ |
my @tools; |
403 |
+ |
|
404 |
+ |
map |
405 |
+ |
{ |
406 |
+ |
if ($_ ne "self") |
407 |
+ |
{ |
408 |
+ |
push(@tools, $_); |
409 |
+ |
} |
410 |
+ |
} keys %{$self->{SETUP}}; |
411 |
+ |
|
412 |
+ |
# Return list of set-up tools: |
413 |
+ |
return @tools; |
414 |
+ |
} |
415 |
+ |
|
416 |
+ |
sub toolsdata() |
417 |
+ |
{ |
418 |
+ |
my $self = shift; |
419 |
+ |
my $tooldata = []; |
420 |
+ |
my $rawsel = $self->selected(); |
421 |
+ |
|
422 |
+ |
foreach my $tool ( sort { %{$rawsel}->{$a} |
423 |
+ |
<=> %{$rawsel}->{$b}} |
424 |
+ |
keys %{$rawsel} ) |
425 |
+ |
{ |
426 |
+ |
# Return tool data objects of all set-up tools, skipping the tool "self": |
427 |
+ |
if ($_ ne "self") |
428 |
+ |
{ |
429 |
+ |
# Keep only tools that have really been set up: |
430 |
+ |
if (exists $self->{SETUP}->{$tool}) |
431 |
+ |
{ |
432 |
+ |
push(@tooldata,$self->{SETUP}->{$tool}); |
433 |
+ |
} |
434 |
+ |
} |
435 |
+ |
} |
436 |
+ |
|
437 |
+ |
# Return the array of tools, in order that they appear in RequirementsDoc: |
438 |
+ |
return @tooldata; |
439 |
+ |
} |
440 |
+ |
|
441 |
+ |
sub definedtool() |
442 |
+ |
{ |
443 |
+ |
my $self=shift; |
444 |
+ |
my ($tool)=@_; |
445 |
+ |
|
446 |
+ |
# Check to see if tool X is an external tool: |
447 |
+ |
grep ($_ eq $tool, keys %{$self->{SETUP}}) ? return 1 |
448 |
+ |
: return 0; |
449 |
+ |
} |
450 |
+ |
|
451 |
+ |
sub checkifsetup() |
452 |
+ |
{ |
453 |
+ |
my $self=shift; |
454 |
+ |
my ($tool)=@_; |
455 |
+ |
# Return the ToolData object if the tool has been set up: |
456 |
+ |
(exists $self->{SETUP}->{$tool}) ? return ($self->{SETUP}->{$tool}) |
457 |
+ |
: return undef; |
458 |
+ |
} |
459 |
+ |
|
460 |
+ |
sub cloned_tm() |
461 |
+ |
{ |
462 |
+ |
my $self=shift; |
463 |
+ |
# Has this area already been cloned and brought in-line with current location: |
464 |
+ |
@_ ? $self->{CLONED} = $_[0] |
465 |
+ |
: $self->{CLONED}; |
466 |
+ |
} |
467 |
+ |
|
468 |
+ |
sub remove_tool() |
469 |
+ |
{ |
470 |
+ |
my $self=shift; |
471 |
+ |
my ($toolname)=@_; |
472 |
+ |
my $tools = $self->{SETUP}; |
473 |
+ |
my $newtlist = {}; |
474 |
+ |
|
475 |
+ |
while (my ($tool, $tooldata) = each %$tools) |
476 |
+ |
{ |
477 |
+ |
if ($tool ne $toolname) |
478 |
+ |
{ |
479 |
+ |
$newtlist->{$tool} = $tooldata; |
480 |
+ |
} |
481 |
+ |
else |
482 |
+ |
{ |
483 |
+ |
print "Deleting $toolname from cache.","\n"; |
484 |
+ |
} |
485 |
+ |
} |
486 |
+ |
|
487 |
+ |
$self->{SETUP} = $newtlist; |
488 |
+ |
|
489 |
+ |
# Now remove from the RAW tool list: |
490 |
+ |
$self->cleanup_raw($toolname); |
491 |
+ |
|
492 |
+ |
print "ToolManager: Updating tool cache.","\n"; |
493 |
+ |
$self->writecache(); |
494 |
+ |
} |
495 |
+ |
|
496 |
+ |
sub scram_projects() |
497 |
+ |
{ |
498 |
+ |
my $self=shift; |
499 |
+ |
my $scram_projects={}; |
500 |
+ |
|
501 |
+ |
foreach my $t ($self->tools()) |
502 |
+ |
{ |
503 |
+ |
# Get the ToolData object: |
504 |
+ |
my $td=$self->{SETUP}->{$t}; |
505 |
+ |
$scram_projects->{$t}=$td->variable_data(uc($t)."_BASE"), if ($td->scram_project()); |
506 |
+ |
} |
507 |
+ |
|
508 |
+ |
return $scram_projects; |
509 |
+ |
} |
510 |
+ |
|
511 |
+ |
1; |