-
Notifications
You must be signed in to change notification settings - Fork 12
/
build_interface.py
executable file
·2237 lines (1957 loc) · 88.9 KB
/
build_interface.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
"""
This module implements building the ECMWF BUFR library and creation of
a python interface around the BUFR library provided by
ECMWF to allow reading and writing the WMO BUFR file standard.
"""
# #[ documentation
#
# Note about the use of the "# #[" and "# #]" comments:
# these are folding marks for my favorite editor, emacs, combined with its
# folding mode
# (see http://www.emacswiki.org/emacs/FoldingMode for more details)
# Please do not remove them.
#
# For details on the revision history, refer to the log-notes in
# the mercurial revisioning system hosted at google code.
#
# Written by: J. de Kloe, KNMI (www.knmi.nl),
#
# Copyright J. de Kloe
# This software is licensed under the terms of the LGPLv3 Licence
# which can be obtained from https://www.gnu.org/licenses/lgpl.html
#
# #]
# #[ imported modules
from __future__ import (absolute_import, division,
print_function) # , unicode_literals)
import os # operating system functions
import sys # operating system functions
import glob # allow for filename expansion
import tarfile # handle tar archives
import shutil # portable file copying functions
import subprocess # support running additional executables
import stat # handling of file stat data
import datetime # date handling functions
from pybufr_ecmwf.helpers import (get_and_set_the_module_path,
get_software_root)
from pybufr_ecmwf.custom_exceptions import (ProgrammingError,
LibraryBuildError,
InterfaceBuildError)
# not used: BuildException
# #]
# #[ constants
# the first one found will be used, unless a preferred one is specified.
POSSIBLE_F_COMPILERS = ['gfortran', 'g95', 'g77',
'f90', 'f77', 'ifort', 'pgf90', 'pgf77']
POSSIBLE_C_COMPILERS = ['gcc', 'icc', 'cc']
# define common compiler flags for each compiler
# (also used for the custom case)
FFLAGS_COMMON = ['-O', '-Dlinux', '-fPIC']
CFLAGS_COMMON = ['-O', '-fPIC']
# define specific compiler flags for each compiler
# needed to build the ECMWF BUFR library
FFLAGS_NEEDED = {'g95': ['-i4', '-r8', '-fno-second-underscore'],
'gfortran': ['-fno-second-underscore', ],
'gfortran_10': ['-fno-second-underscore',
'-fallow-argument-mismatch',],
'g77': ['-i4', ],
'f90': ['-i4', ],
'f77': ['-i4', ],
'pgf90': ['-i4', ],
'pgf77': ['-i4', ],
'ifort': ['-i4', ]}
CFLAGS_NEEDED = {'gcc': [],
'icc': [],
'cc': []}
for k in FFLAGS_NEEDED:
FFLAGS_NEEDED[k].extend(FFLAGS_COMMON)
for k in CFLAGS_NEEDED:
CFLAGS_NEEDED[k].extend(CFLAGS_COMMON)
SO_FILE_PATTERN = 'ecmwfbufr.cpython*.so'
# #]
# #[ some helper functions
def rem_quotes(txt):
# #[
""" a little helper function to remove quotes from a string."""
if txt is None:
return txt
elif txt[0] == "'" and txt[-1] == "'":
return txt[1:-1]
elif txt[0] == '"' and txt[-1] == '"':
return txt[1:-1]
else:
return txt
# #]
def ensure_permissions(filename, mode):
# #[ ensure permissions for "world"
""" a little routine to ensure the permissions for the
given file are as expected """
file_stat = os.stat(filename)
current_mode = stat.S_IMODE(file_stat.st_mode)
new_mode = None
if mode == 'r':
new_mode = current_mode | int("444", 8)
if mode == 'w':
new_mode = current_mode | int("222", 8)
if mode == 'x':
new_mode = current_mode | int("111", 8)
if mode == 'rx':
new_mode = current_mode | int("555", 8)
if new_mode:
os.chmod(filename, new_mode)
else:
print('ERROR in ensure_permissions: unknown mode string: ', mode)
raise ProgrammingError
# #]
def run_shell_command(cmd, libpath=None, catch_output=True,
module_path='./', verbose=True):
# #[
""" a wrapper routine around subprocess.Popen intended
to make it a bit easier to call this functionality.
Options:
-libpath: add this path to the LD_LIBRARY_PATH environment variable
before executing the subprocess
-catch_output: if True, this function returns 2 lists of text lines
containing the stdout and stderr of the executed subprocess
-verbose: give some feedback to the user while executing the
code (usefull for debugging)"""
# get the list of already defined env settings
env = os.environ
if libpath:
# add the additional env setting
envname = "LD_LIBRARY_PATH"
if envname in env:
env[envname] = env[envname] + ":" + libpath
else:
env[envname] = libpath
if 'PYTHONPATH' in env:
env['PYTHONPATH'] = env['PYTHONPATH']+':'+module_path
else:
env['PYTHONPATH'] = module_path
if verbose:
print("Executing command: ", cmd)
if catch_output:
# print('env[PYTHONPATH] = ', env['PYTHONPATH'])
subpr = subprocess.Popen(cmd, shell=True, env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
# wait until the child process is done
# subpr.wait() # seems not necessary when catching stdout and stderr
if sys.version_info[0] == 2:
lines_stdout = subpr.stdout.readlines()
lines_stderr = subpr.stderr.readlines()
elif sys.version_info[0] == 3:
# in python 3 the readlines() method returns bytes,
# so convert them to a unicode string for convenience
tmp_lines_stdout = subpr.stdout.readlines()
tmp_lines_stderr = subpr.stderr.readlines()
lines_stdout = []
lines_stderr = []
for line in tmp_lines_stdout:
lines_stdout.append(line.decode('utf-8'))
for line in tmp_lines_stderr:
lines_stderr.append(line.decode('utf-8'))
else:
errtxt = 'This python version is not supported: '+sys.version
raise NotImplementedError(errtxt)
#print("lines_stdout: ", lines_stdout)
#print("lines_stderr: ", lines_stderr)
return (lines_stdout, lines_stderr)
else:
subpr = subprocess.Popen(cmd, shell=True, env=env)
# wait until the child process is done
subpr.wait()
return
# #]
def fortran_compile_and_execute(fcmp, fflags, f_code, f_executable, f_libpath):
# #[
""" convenience routine to compile and execute a bit of fortran code,
and to return the stdout and stderr generated by the just compiled code.
"""
f_file = f_executable+".F90"
tfd = open(f_file, 'w')
tfd.write(f_code)
tfd.close()
# contruct the compile command
cmd = fcmp+' '+fflags+' -o '+f_executable+' '+f_file
# now issue the compile command
if f_libpath == "":
print("Executing command: ", cmd)
os.system(cmd)
else:
run_shell_command(cmd, libpath=f_libpath, catch_output=False)
# now execute the just generated test program to verify if we succeeded
# add a './' to ensure the executable is also found for users that
# do not have '.' in their default search path
cmd = os.path.join('.', f_executable)
if f_libpath == "":
(lines_stdout, lines_stderr) = run_shell_command(cmd)
else:
(lines_stdout, lines_stderr) = \
run_shell_command(cmd, libpath=f_libpath)
# clean up
os.remove(f_file)
return (lines_stdout, lines_stderr)
# #]
def fortran_compile_test(fcmp, fflags, f_libpath):
# #[
""" a method to check if we really have some fortran compiler
installed (it writes a few lines of fortran, tries to compile
it, and compares the output with the expected output) """
# Note: for now the flags are not used in these test because these
# are specific for generating a shared-object file, and will fail to
# generate a simple executable for testing
fortran_test_code = \
"""
program pybufr_test_program
print *,'Hello pybufr module:'
print *,'Fortran compilation seems to work fine ...'
end program pybufr_test_program
"""
# generate a testfile with a few lines of Fortran90 code
fortran_test_executable = "pybufr_fortran_test_program"
(lines_stdout, lines_stderr) = \
fortran_compile_and_execute(fcmp, fflags, fortran_test_code,
fortran_test_executable,
f_libpath)
expected_output = [' Hello pybufr module:\n',
' Fortran compilation seems to work fine ...\n']
if ((expected_output[0] not in lines_stdout) or
(expected_output[1] not in lines_stdout)):
print("ERROR: Fortran compilation test failed; "+
"something seems very wrong")
print("Expected output: ", expected_output)
print('actual output stdout = ', lines_stdout)
print('actual output stderr = ', lines_stderr)
raise EnvironmentError
print("Fortran compilation test successfull...")
# clean up
os.remove(fortran_test_executable)
# #]
def c_compile_and_execute(ccmp, cflags, c_code, c_executable, c_libpath):
# #[
""" convenience routine to compile and execute a bit of c code,
and to return the stdout and stderr generated by the just compiled code.
"""
# Note: for now the flags are not used in these test because these
# are specific for generating a shared-object file, and will fail to
# generate a simple executable for testing
# libpath may point to a custom LD_LIBRARY_PATH setting
# needed to run the compiler
c_file = c_executable+".c"
tfd = open(c_file, 'w')
tfd.write(c_code)
tfd.close()
# contruct the compile command
cmd = ccmp+' '+cflags+' -o '+c_executable+' '+c_file
# now issue the compile command
if c_libpath == "":
print("Executing command: ", cmd)
os.system(cmd)
else:
run_shell_command(cmd, libpath=c_libpath, catch_output=False)
# now execute the just generated program
# add a './' to ensure the executable is also found for users that
# do not have '.' in their default search path
cmd = os.path.join('.', c_executable)
if c_libpath == "":
(lines_stdout, lines_stderr) = run_shell_command(cmd)
else:
(lines_stdout, lines_stderr) = run_shell_command(cmd, libpath=c_libpath)
# clean up
os.remove(c_file)
return (lines_stdout, lines_stderr)
# #]
def c_compile_test(ccmp, cflags, c_libpath):
# #[
""" a method to check if we really have some c compiler
installed (it writes a few lines of c, tries to compile
it, and compares the output with the expected output) """
c_test_code = \
r"""
#include <stdio.h>
int main()
{
printf("Hello pybufr module:\n");
printf("c compilation seems to work fine ...\n");
return 0;
}
"""
# generate a testfile with a few lines of Fortran90 code
c_test_executable = "pybufr_c_test_program"
(lines_stdout, lines_stderr) = \
c_compile_and_execute(ccmp, cflags, c_test_code,
c_test_executable, c_libpath)
expected_output = ['Hello pybufr module:\n',
'c compilation seems to work fine ...\n']
if ((expected_output[0] not in lines_stdout) or
(expected_output[1] not in lines_stdout)):
print("ERROR: c compilation test failed; something seems very wrong")
print("Expected output: ", expected_output)
print('actual output stdout = ', lines_stdout)
print('actual output stderr = ', lines_stderr)
raise EnvironmentError
print("c compilation test successfull...")
# clean up
os.remove(c_test_executable)
# #]
def retrieve_integer_sizes(ccmp, cflags, c_libpath,
fcmp, fflags, f_libpath):
# #[
""" some trickery to retrieve the currently used integer variable
sizes in both fortran and c
"""
#CSIZEINT=`../support/GetByteSizeInt`
#CSIZELONG=`../support/GetByteSizeLong`
#F90SIZEINT=`../support/GetByteSizeDefaultInteger`
c_code = \
r"""
#include <stdio.h>
int main()
{
int testinteger;
printf("%zu\n",sizeof(testinteger));
return 0;
}
"""
c_executable = 'GetByteSizeInt'
lines_stdout = c_compile_and_execute(ccmp, cflags, c_code,
c_executable, c_libpath)[0]
bytesizeint = lines_stdout[0].strip()
c_code = \
r"""
#include <stdio.h>
int main()
{
long testinteger;
printf("%zu\n",sizeof(testinteger));
return 0;
}
"""
c_executable = 'GetByteSizeLong'
lines_stdout = c_compile_and_execute(ccmp, cflags, c_code,
c_executable, c_libpath)[0]
bytesizelong = lines_stdout[0].strip()
f90_code = \
r"""
program GetByteSizeDefaultInteger
integer :: default_integer, nbytes_default_integer
inquire(iolength=nbytes_default_integer) default_integer
print *,nbytes_default_integer
end program GetByteSizeDefaultInteger
"""
f90_executable = 'GetByteSizeDefaultInteger'
lines_stdout = \
fortran_compile_and_execute(fcmp, fflags, f90_code,
f90_executable, f_libpath)[0]
try:
bytesizedefaultinteger = lines_stdout[0].strip()
except IndexError:
bytesizedefaultinteger = None
if bytesizedefaultinteger is None:
# try again, now defining nbytes_default_integer explicitely
# as 8-byte integer, which seems needed if you compile
# with g95-64 bit version combined with the -i4 option
f90_code = \
r"""
program GetByteSizeDefaultInteger
integer :: default_integer
integer*8 :: nbytes_default_integer
inquire(iolength=nbytes_default_integer) default_integer
print *,nbytes_default_integer
end program GetByteSizeDefaultInteger
"""
f90_executable = 'GetByteSizeDefaultInteger'
lines_stdout = \
fortran_compile_and_execute(fcmp, fflags, f90_code,
f90_executable, f_libpath)[0]
try:
bytesizedefaultinteger = lines_stdout[0].strip()
except IndexError:
bytesizedefaultinteger = None
if bytesizedefaultinteger is None:
txt = 'ERROR: could not retrieve bytesizedefaultinteger '+\
'for this fortran compiler: '+fcmp
raise ProgrammingError(txt)
# print('GetByteSizeInt: ',bytesizeint)
# print('GetByteSizeLong: ',bytesizelong)
# print('GetByteSizeDefaultInteger: ',bytesizedefaultinteger)
return (bytesizeint, bytesizelong, bytesizedefaultinteger)
# #]
def insert_pb_interface_definition(sfd, integer_sizes):
# #[
""" the pb interface routines are mostly written in c, so f2py
will not automatically generate their signature. This
subroutine explicitely adds these signatures.
"""
#(ByteSizeInt, ByteSizeLong, ByteSizeDefaultInteger) = integer_sizes
bytesizelong = integer_sizes[1]
#intlen = None
#if ByteSizeDefaultInteger == ByteSizeInt:
# intlen = ByteSizeInt # = 4 bytes
#if ByteSizeDefaultInteger == ByteSizeLong:
# intlen = ByteSizeLong # = 8 bytes
intlen = bytesizelong # = 8 bytes
print('Using intlen = ', intlen, ' to build the pbio interface')
indentation = 8*' '
lines_to_add = \
["subroutine pbopen(cFileUnit,BufrFileName,mode,bufr_error_flag)",
#" intent(c) pbopen"
#" intent(c)"
" integer*"+intlen+", intent(out) :: cFileUnit",
" character(len=*), intent(in) :: BufrFileName",
" character(len=1), intent(in) :: mode",
" integer*"+intlen+", intent(out) :: bufr_error_flag",
"end subroutine pbopen",
"subroutine pbclose(cFileUnit,bufr_error_flag)",
" integer*"+intlen+", intent(inplace) :: cFileUnit",
" integer*"+intlen+", intent(inplace) :: bufr_error_flag ",
"end subroutine pbclose",
# this one is implemented in Fortran, and is handled by
# adapt_f2py_signature_file defined next, so manual fix is needed for it.
# "subroutine pbbufr(cFileUnit,Buffer,BufferSizeBytes,MsgSizeBytes,&",
# " bufr_error_flag)",
# " integer*"+intlen+", intent(inplace) :: cFileUnit",
# " integer*"+intlen+",dimension(*), intent(inplace) :: Buffer",
# " integer*"+intlen+", intent(inplace) :: BufferSizeBytes",
# " integer*"+intlen+", intent(inplace) :: MsgSizeBytes",
# " integer*"+intlen+", intent(inplace) :: bufr_error_flag ",
# "end subroutine pbbufr",
"subroutine pbwrite(cFileUnit,Buffer,MsgSizeBytes,bufr_return_value)",
" integer*"+intlen+", intent(inplace) :: cFileUnit",
" integer*"+intlen+",dimension(*), intent(inplace) :: Buffer",
" integer*"+intlen+", intent(inplace) :: MsgSizeBytes",
" integer*"+intlen+\
", intent(inplace) :: bufr_return_value",
"end subroutine pbwrite",
"subroutine pbseek(cFileUnit,offset,whence,bufr_return_value)",
" integer*"+intlen+", intent(in) :: cFileUnit",
" integer*"+intlen+", intent(in) :: offset",
" integer*"+intlen+", intent(in) :: whence",
" integer*"+intlen+", intent(out) :: bufr_return_value",
"end subroutine pbseek"]
print("Inserting hardcoded interface to pbio routines in "+
"signatures file ...")
for lta in lines_to_add:
sfd.write(indentation+lta+'\n')
# #]
def adapt_f2py_signature_file(signature_file, integer_sizes, set_jelem,
verbose=False):
# #[
"""
some code to adapt the signature file generated by the f2py tool.
Regrettably this is needed since this tool seems not to handle
constant parameters defined in include files properly.
"""
# NOTE: maybe this modification is not needed if I can get the file
# with the parameters included in an other way.
# Looking at the f2py manpage the option -include might do the
# trick but that one is depricated. In stead a usercode section
# should be used, but that again means modifying the signature
# file ...
# Also the --include_paths option might be related.
# TODO: sort this out (handling of constant parameters by f2py)
#signature_file = "f2py_build/signatures.pyf"
# these values are defined in parameter.F
# PARAMETER(JSUP = 9,
# JSEC0= 3,
# JSEC1= 40,
# JSEC2=4096,
# JSEC3= 4
# JSEC4= 2,
# JELEM=320000,
# JSUBS=400,
# JCVAL=150 ,
# JBUFL=512000,
# JBPW = 32,
# JTAB =3000,
# JCTAB=3000,
# JCTST=9000,
# JCTEXT=9000,
# JWORK=4096000,
# JKEY=46,
# JTMAX=10,
# JTCLAS=64,
# JTEL=255)
# WARNING:
# these numbers really should NOT be hardcoded here
# but extracted from the fortran code.
# However, at this point in time the interface to
# fortran is not yet available, so for now use this
# quick and dirty workaround...
edits = {}
edits['JSUP'] = 9
edits['JSEC0'] = 3
edits['JSEC1'] = 40
edits['JSEC2'] = 4096
edits['JSEC3'] = 4
edits['JSEC4'] = 2
if set_jelem is not None:
edits['JELEM'] = str(set_jelem)
else:
edits['JELEM'] = 320000
edits['JSUBS'] = 400
edits['JCVAL'] = 150
edits['JBUFL'] = 512000
edits['JBPW'] = 32
edits['JTAB'] = 3000
edits['JCTAB'] = 3000
edits['JCTST'] = 9000
edits['JCTEXT'] = 9000
edits['JWORK'] = 4096000
edits['JKEY'] = 46
edits['JTMAX'] = 10
edits['JTCLAS'] = 64
edits['JTEL'] = 255
# edits[''] =
# read the file
lines = open(signature_file).readlines()
# create a backup copy, to allow manual inspection
source = signature_file
destination = signature_file+".bak"
shutil.copyfile(source, destination)
print("Fixing array size definitions in signatures definition ...")
sfd = open(signature_file, "w")
inside_subroutine = False
inside_retrieve_settings = False
inside_pbbufr_sign = False
for line in lines:
mod_line = line
if 'end subroutine' in mod_line:
inside_subroutine = False
elif 'subroutine' in mod_line:
inside_subroutine = True
if 'end subroutine retrieve_settings' in mod_line:
inside_retrieve_settings = False
elif 'subroutine retrieve_settings' in mod_line:
inside_retrieve_settings = True
if 'end subroutine pbbufr' in mod_line:
inside_pbbufr_sign = False
elif 'subroutine pbbufr' in mod_line:
inside_pbbufr_sign = True
if inside_subroutine:
if ' ::' in mod_line:
# Add the intent(inplace) switch to all subroutine
# parameters.
# This might not be very pretty, but otherwise all
# parameters are assigned the default, which is intent(in).
# Maybe the proper way would be to sort out for each routine
# in this library which parameters are intent(in) and
# which are intent(out), but this is a huge task (and
# should be done by ECMWF rather then by us I think...)
if not 'intent(out)' in mod_line:
# do this only for code that has no explicit intent(out)
(part1, part2) = mod_line.split(' ::')
if inside_retrieve_settings:
# explicitely add intent(out)
# this seems needed for the python3 case!
mod_line = part1+',intent(out) ::'+part2
else:
mod_line = part1+',intent(inplace) ::'+part2
if inside_pbbufr_sign:
# fix a bug in the pbbufr.F fortran code that causes f2py to
# fail on interfacing to this routine
if 'integer dimension(1),intent(inplace) :: karray' in mod_line:
mod_line = mod_line.replace('dimension(1)', 'dimension(*)')
if 'dimension' in mod_line:
for edit in edits:
# the value inside the dimension() spec
# in the signature file sometimes has extra braces.
# sometimes not (depending on the f2py version).
# and in case of 2 dimensions, the value to be replaced
# ends with a comma.
# So at least 3 cases are to be considered here.
txt1 = '(('+edit.lower()+'))'
txt2 = '('+edit.lower()+')'
txt3 = '('+edit.lower()+','
value1 = '('+str(edits[edit])+')'
value2 = '('+str(edits[edit])+')'
value3 = '('+str(edits[edit])+','
if txt1 in mod_line:
mod_line = mod_line.replace(txt1, value1)
elif txt2 in mod_line:
mod_line = mod_line.replace(txt2, value2)
elif txt3 in mod_line:
mod_line = mod_line.replace(txt3, value3)
if mod_line.strip() == "end interface":
# NOTE: the pb interface routines are written in c, so f2py
# will not automatically generate their signature. This next
# subroutine call explicitely adds these signatures.
insert_pb_interface_definition(sfd, integer_sizes)
if verbose:
if mod_line != line:
print("adapting line: ", line)
print("to : ", mod_line)
sfd.write(mod_line)
sfd.close()
# #]
def descend_dirpath_and_find(input_dir, glob_pattern):
# #[
"""
a little helper routine that steps down the different components
of the provided directory path, and tests for the presence of
a file that matches the given glob pattern.
If a match is found, the directory in which the match is present,
and a list of matching files is returned.
If no match is found a tuple with two None values is returned.
"""
absdirname = os.path.abspath(input_dir)
# print('start: absdirname = ',absdirname)
while absdirname != "/":
pattern = os.path.join(absdirname, glob_pattern)
filelist = glob.glob(pattern)
if len(filelist) > 0:
# print('descend_dirpath_and_find succeeded: result')
# print('(absdirname, filelist) = ',(absdirname, filelist))
return (absdirname, filelist)
base = os.path.split(absdirname)[0]
absdirname = base
# print('next: absdirname = ',absdirname)
# print('descend_dirpath_and_find failed: no result found')
return (None, None)
# #]
def extract_version():
# #[
""" a little function to extract the module version from
the setup.py script, and if present, extract the mercurial
revision from the hg repository, and store it in a place where
the user can access it.
"""
# assume we are inside the pybufr_ecmwf module dir
# when this function is executed.
# retrieve the software version
software_version = 'unknown'
(setuppath, setupfiles) = descend_dirpath_and_find(os.getcwd(), 'setup.py')
if not setuppath:
print('ERROR: could not locate setup.py script needed to extract')
print('ERROR: the current software version')
raise ProgrammingError
for line in open(os.path.join(setuppath, setupfiles[0])).readlines():
if 'version=' in line:
quoted_version = line.split('=')[1].replace(',', '')
software_version = quoted_version.replace("'", '').strip()
# retrieve mercurial revision if possible
hg_version = 'undefined'
if os.path.exists('.hg'):
cmd = 'hg log -l 1'
lines_stdout = run_shell_command(cmd)[0]
for line in lines_stdout:
if 'changeset:' in line:
hg_version = line.split()[1]
# retrieve git revision if possible
git_version = 'undefined'
if os.path.exists('.git'):
cmd = 'git log -n 1'
lines_stdout = run_shell_command(cmd)[0]
for line in lines_stdout:
if 'commit:' in line:
git_version = line.split()[1]
# retrieve the install date (i.e. todays date)
# current date formatted as: 07-aug-2009
install_date = datetime.date.today().strftime("%d-%b-%Y")
# store the result
version_file = 'version.py'
fds = open(version_file, 'w')
fds.write("software_version = '"+software_version+"'\n")
fds.write("hg_version = '"+hg_version+"'\n")
fds.write("git_version = '"+git_version+"'\n")
fds.write("install_date = '"+install_date+"'\n")
fds.write("version = '"+software_version+'; '+\
hg_version+'; '+install_date+"'\n")
fds.close()
ensure_permissions(version_file, 'rx')
# #]
def symlink_to_all_files(source_dir, dest_dir):
# #[ create symlinks in dest_dir for all links in source_dir
'''
a helper routine to create symbolic links to all available
BUFR tables in the dest_dir directory.
'''
filelist_b = glob.glob(os.path.join(source_dir, 'B*'))
filelist_c = glob.glob(os.path.join(source_dir, 'C*'))
filelist_d = glob.glob(os.path.join(source_dir, 'D*'))
filelist = filelist_b
filelist.extend(filelist_c)
filelist.extend(filelist_d)
# first copy the real files
for fnm in filelist:
filename = os.path.split(fnm)[1]
if not os.path.islink(fnm):
dest = os.path.join(dest_dir, filename)
shutil.copy(fnm, dest)
# and make sure they are world readable
# to ensure the module can be used by all, even if
# the setup.py build was done as root or using sudo
os.chmod(dest, 0o0644)
# then create all symlinks
links = []
for fnm in filelist:
filename = os.path.split(fnm)[1]
if os.path.islink(fnm):
realname = os.path.realpath(fnm)
realfilename = os.path.split(realname)[1]
links.append((realfilename, filename))
cwd = os.getcwd()
os.chdir(dest_dir)
for (realfilename, filename) in links:
os.symlink(realfilename, filename)
os.chdir(cwd)
# print(filelist)
# sys.exit(1)
# #]
def generate_c_underscore_wrapper(source_dir):
# #[ create macosx wrapper code
print('starting generate_c_underscore_wrapper')
source_file_list = source_dir+"/bufrdc/*.F "+\
source_dir+"/pbio/pbbufr.F"
# print(source_file_list)
wrapper_name = source_dir+"/pbio/c_macosx_wrapper.c"
fd_out = open(wrapper_name,'w')
for pattern in source_file_list.split():
for fortran_file in glob.glob(pattern):
cmd = 'grep -i subroutine '+fortran_file
(lines_stdout, lines_stderr) = run_shell_command(cmd, verbose=False)
# these lines are unicode strings in python3
for line in lines_stdout:
def_line = line.strip()
#print('['+def_line+']')
if def_line=='':
continue
if def_line[0]!='C':
subr_name = def_line.split()[1].split('(')[0]
#print('file: '+fortran_file,
# 'subr_name: '+subr_name)
fd_out.write('extern void *{0}_();\n'.\
format(subr_name))
fd_out.write('void *_{0}_() {{ return {1}_(); }}\n\n'.\
format(subr_name, subr_name))
# manually add the 4 needed c-functions from pbio.c
for c_func in ['pbopen', 'pbclose', 'pbwrite', 'pbseek']:
fd_out.write('extern void *{0}();\n'.\
format(c_func))
fd_out.write('void *_{0}() {{ return {1}(); }}\n\n'.\
format(c_func, c_func))
fd_out.close()
print('Created wrapper: '+wrapper_name)
return wrapper_name
# #]
# #]
class InstallBUFRInterfaceECMWF(object):
# #[ the main builder class
"""
a class that builds the interface between the ECMWF
BUFR library and python,
"""
def __init__(self, verbose=False,
preferred_fortran_compiler=None,
preferred_c_compiler=None,
fortran_compiler=None,
fortran_ld_library_path=None,
fortran_flags=None,
c_compiler=None,
c_ld_library_path=None,
c_flags=None,
set_jelem=None,
debug_f2py_c_api=False):
# #[
# first remove any quotes that may be around the strings
# (this may happen if the user uses quotes in the setup.cfg file)
self.preferred_fortran_compiler = rem_quotes(preferred_fortran_compiler)
self.preferred_c_compiler = rem_quotes(preferred_c_compiler)
self.fortran_compiler = rem_quotes(fortran_compiler)
self.fortran_ld_library_path = rem_quotes(fortran_ld_library_path)
self.c_compiler = rem_quotes(c_compiler)
self.c_ld_library_path = rem_quotes(c_ld_library_path)
self.fortran_flags = rem_quotes(fortran_flags)
self.c_flags = rem_quotes(c_flags)
self.set_jelem = set_jelem
self.debug_f2py_c_api = debug_f2py_c_api
# save the verbose setting
self.verbose = verbose
# save the location to be used for installing the ECMWF BUFR library
self.ecmwf_bufr_lib_dir = "./ecmwf_bufr_lib"
# define the names of the library and shared object files
# that will be created by this class
self.bufr_lib_file = "libbufr.a"
self.wrapper_build_dir = "f2py_build"
self.wrapper_module_name = "ecmwfbufr"
# init other module attributes to None
self.fortran_compiler_to_use = None
self.c_compiler_to_use = None
# variable to store current integer sizes in c and Fortran
self.integer_sizes = None
# #]
def build(self):
# #[
"""a method to start building the BUFR interface"""
bufr_was_build = False
wrapper_was_build = False
# check for the presence of the library
if os.path.exists(self.bufr_lib_file):
print("BUFR library seems present")
else:
print("Entering installation sequence:")
self.install()
print("compilation of BUFR library finished")
bufr_was_build = True
try:
wrapper_name = glob.glob(SO_FILE_PATTERN)[0]
except IndexError:
wrapper_name = 'undefined'
if os.path.exists(wrapper_name):
print("python wrapper seems present")
else:
print("Entering wrapper generation sequence:")
source_dir = self.get_source_dir()[0]
self.generate_python_wrapper(source_dir)
print("compilation of library wrapper finished")
wrapper_was_build = True
if (not bufr_was_build) and (not wrapper_was_build):
print("\nNothing to do\n"+
"Execute the clean.py tool if you wish to start again "+
"from scratch.")
print('extracting library constants')
self.extract_constants()
print('storing version info')
extract_version()
print('copying sample files')
self.copy_sample_files()
# #]
def rebuild(self):
# #[ rebuild the software
""" same as install, but always run the make, even if the
wrapper library already seems present"""
self.install(remake=True)
source_dir = self.get_source_dir()[0]
self.generate_python_wrapper(source_dir, remake=True)
# #]
def clean(self):
# #[
""" a method to clean-up things that I don't want to have
included in the binary/rpm distributions."""
# if verbose is set this signals we are debugging the
# code, so do not remove temp folders in that case
#if self.verbose:
return
# this is a bit of a dirty hack.
# It removes the subdir ecmwf_bufr_lib and everything below
# to prevent it to be included in the binary/rpm distributions
# There should be a nicer way to do this, but I have not
# yet found it ...
dirs_to_remove = [self.ecmwf_bufr_lib_dir,
self.wrapper_build_dir]
for dir_to_remove in dirs_to_remove:
if os.path.exists(dir_to_remove):
cmd = r'\rm -rf '+dir_to_remove
print("executing command: ", cmd)
os.system(cmd)
# #]
def use_bundled_library_copy(self):
# #[
""" copy the bundled version
of the library sources stored in ecmwf_bufr_lib_sources.
We must descend the directory tree first to find the root
before doing this copy. """
# make sure the destination dir exists
if not os.path.exists(self.ecmwf_bufr_lib_dir):
os.makedirs(self.ecmwf_bufr_lib_dir)
cwd = os.getcwd()
absdirname = os.path.abspath(cwd)
while absdirname != "/":
files = os.listdir(absdirname)
if "setup.cfg" in files:
pattern = os.path.join(absdirname,
'ecmwf_bufr_lib_sources',
'bufrdc_000*.tar.gz')
tgz_filelist = glob.glob(pattern)
if len(tgz_filelist) > 0:
tgz_file = tgz_filelist[0]
cmd = 'cp '+tgz_file+' '+self.ecmwf_bufr_lib_dir
print("Executing command: ", cmd)
os.system(cmd)
break
base = os.path.split(absdirname)[0]
absdirname = base
# return to the original location
os.chdir(cwd)
# #]
def use_bundled_tables_dir_copy(self):
# #[
""" copy the bundled version of the updated bufr tables
in ecmwf_bufr_lib_sources.
We must descend the directory tree first to find the root
before doing this copy. """
# make sure the destination dir exists
if not os.path.exists(self.ecmwf_bufr_lib_dir):
os.makedirs(self.ecmwf_bufr_lib_dir)