[M3devel] compiler switch "reduce target variation"?
Jay K
jayk123 at hotmail.com
Tue Jul 4 08:34:03 CEST 2017
Trying to resume work here..
Here, I've commited this to a branch:
https://github.com/modula3/cm3/commit/0d59546d11641afe3772b73f226c2e90b960d7fc
There are some unrelated changes I will tease out and commit separately.
I still believe we should implement this.
It is a variation of what people call "deterministic builds".
"deterministic builds" are kind of an overloaded term, or common sense at first, but then controversial as details emerge.
For examples:
- Compilers should not generate random numbers, at least not base their output on them.
- Optimizers should not "give up" after some elapsed time or consumed memory -- leading to different results on similar-but-different hardware -- though if memory can be accounted precisely and controlled by a switch, maybe
But then again, compilers should make good use of their host machines -- you can't have it both ways.
- full paths should occur nowhere in the output
see also debugging information, that might work better with full paths -- no need to tell debugger some such search paths
- varying a source file's full path should not change how it compiles; but see constructs like #include ../foo.c and printf(__FILE__) or printf("%d\n", sizeof(__FILE__)). And is __FILE__ a full path or not? (Modula-3 has the analogous Compiler.ThisLine and Compiler.ThisFile so please pardon my C++-ness, it still is relevant
Perhaps compiler can trim prefixes of paths.
- adding a comment in a heavily used header should not change any output; but see for example, in such header inline void foo() { printf("%d\n", __LINE__);
- adding comments anywhere, really, should not change any output -- but again __LINE__
- recompiling all the same files with the same compiler and command lines should produce the same output every time -- but see for examples printf("%d\n", __TIME__ or __DATE__ or __TIMESTAMP__) or things like Modula-3 date-based-version; burden is perhaps on the programer to not use these problematic constructs, and compiler can maybe get switches to warn/error for them.
So, then, I add to this mix, Linux/x86, NetBSD/x86, OpenBSD/x86, Solaris/x86 should generally have identical output.
But again, things like printing targetname -- including as comments from the C backend.
- Jay
________________________________
From: M3devel <m3devel-bounces at elegosoft.com> on behalf of Jay K <jayk123 at hotmail.com>
Sent: Wednesday, March 15, 2017 8:37 AM
To: m3devel
Subject: [M3devel] compiler switch "reduce target variation"?
Tangent: Should we start use git branches and pull requests?
Are people here reasonable able to view textual diffs? It pushes my limits personally.
But I know some developers work this way.
This is some old stuff I was working on.
I kinda think it should be the default, but almost everyone here protests
almost all of my changes, so here is a new switch instead.
The idea is to remove somewhat gratituous target variation.
One would have to argue over each difference and if it is gratuitous
and if it should be removed by default, or under a switch, and which switch.
So I'm proposing generally one switch to put everything under.
The differences I had noticed in my investigation months ago
is the name of the TARGET appearing, for example in paths.
Such as maybe in generic instantiations, in debug symbols, in output logs.
I actually think debug symbols need full paths, and so for generics,
they end up with target. But for now, I desist on that agenda in favor
of rougly the opposite.
The goal here, as I mentioned before, is to establish that
I386_LINUX == I386_FREEBSD == I386_NETBSD == I386_OPENBSD == I386_SOLARIS
== I386_DARWIN == I386_CYGWIN == I386_SOLARIS maybe == I386_NT
ditto AMD64, SPARC32, SPARC64, PPC32, PPC64, etc.
So that we might collapse targets down.
A soon/later step would establish even fewer equivalence classes:
PP32be == SPARC32, PPC64be == SPARC64, => BigEndian64 or PosixBigEndian64
I386 == ARM => LittleEndian32
AMD64 == ARM64 == PPC64le => LittleEndian64
etc.
and then eventually we could have one or a small number of C-based distributions.
I have to go back and build and test all this anew.
Reasonable?
Maybe the default?
Maybe add a switch for restore-target-difference or preserve-target-differences?
diff --git a/m3-sys/cm3/src/Main.m3 b/m3-sys/cm3/src/Main.m3
index 5d753e6..61b3f18 100644
--- a/m3-sys/cm3/src/Main.m3
+++ b/m3-sys/cm3/src/Main.m3
@@ -5,7 +5,7 @@ MODULE Main;
IMPORT M3Timers, Pathname, Process, Quake;
IMPORT RTCollector, RTParams, RTutils, Thread, Wr;
-IMPORT TextTextTbl;
+IMPORT TextTextTbl, Target;
IMPORT Builder, Dirs, M3Build, M3Options, Makefile, Msg, Utils, WebFile;
IMPORT MxConfig(*, M3Config, CMKey, CMCurrent *);
@@ -18,6 +18,8 @@ VAR
build_dir : TEXT := NIL;
mach : Quake.Machine := NIL;
+CONST BoolToText = ARRAY BOOLEAN OF TEXT{"FALSE", "TRUE"};
+
PROCEDURE DefineIfNotDefined (qmachine: Quake.Machine;
symbol, value: TEXT) RAISES {Quake.Error} =
BEGIN
@@ -73,6 +75,7 @@ VAR defs: TextTextTbl.T;
(* DefineIfNotDefined (mach, "THREAD_LIBRARY", Version.ThreadLibrary); *)
(* DefineIfNotDefined (mach, "WINDOW_LIBRARY", Version.WindowLibrary); *)
DefineIfNotDefined (mach, "WORD_SIZE", MxConfig.HOST_WORD_SIZE);
+ DefineIfNotDefined (mach, "REDUCE_TARGET_VARIATION", BoolToText[Target.ReduceTargetVariation]);
(* Even if the config file overrides the defaults, such as to do
a cross build, the host characteristics are still available. *)
diff --git a/m3-sys/cm3/src/Makefile.m3 b/m3-sys/cm3/src/Makefile.m3
index 64e169e..f2e1c7b 100644
--- a/m3-sys/cm3/src/Makefile.m3
+++ b/m3-sys/cm3/src/Makefile.m3
@@ -5,7 +5,7 @@ MODULE Makefile;
IMPORT FS, M3File, M3Timers, OSError, Params, Process, Text, Thread, Wr;
IMPORT Arg, M3Build, M3Options, M3Path, Msg, Utils, TextSeq, TextTextTbl;
-IMPORT MxConfig, Dirs, Version;
+IMPORT MxConfig, Dirs, Version, Target;
TYPE
NK = M3Path.Kind;
@@ -267,6 +267,9 @@ PROCEDURE ConvertOption (VAR s: State; arg: TEXT; arg_len: INTEGER)
| 'r' => IF Text.Equal(arg, "-realclean") THEN
ok := TRUE; (* mode set during the pre-scan *)
s.found_work := TRUE;
+ ELSIF Text.Equal(arg, "-reduce-target-variation") THEN
+ ok := TRUE;
+ Target.ReduceTargetVariation := TRUE;
END;
| 's' => IF Text.Equal (arg, "-silent") THEN
@@ -707,6 +710,9 @@ CONST
" -group-writable \"",
" -pb <n> allow <n> parallelism in running back-end (experimental)",
" -no-m3ship-resolution use quake variables in .M3SHIP (experimental)",
+ " -reduce-target-variation omit target in some minor places such as",
+ " current working directory in debug information",
+ " for internal development purposes (showing target equivalence)",
"",
"environment variables:",
" M3CONFIG platform dependent configuration file to use (cm3.cfg)",
diff --git a/m3-sys/cminstall/src/config-no-install/cm3cfg.common b/m3-sys/cminstall/src/config-no-install/cm3cfg.common
index 7334252..42db06f 100644
--- a/m3-sys/cminstall/src/config-no-install/cm3cfg.common
+++ b/m3-sys/cminstall/src/config-no-install/cm3cfg.common
@@ -458,8 +458,14 @@ proc GetM3Back() is
else if equal(HOST, TARGET)
m3back = INSTALL_ROOT & "/bin/"
end end
-
+
m3back = "@" & m3back & "cm3cg " & GetM3BackFlags()
+
+ if defined ("REDUCE_TARGET_VARIATION")
+ if REDUCE_TARGET_VARIATION
+ m3back = m3back & " -freduce-target-variation"
+ end
+ end
return m3back
end
diff --git a/m3-sys/m3back/src/M3C.m3 b/m3-sys/m3back/src/M3C.m3
index f6740c7..d584fea 100644
--- a/m3-sys/m3back/src/M3C.m3
+++ b/m3-sys/m3back/src/M3C.m3
@@ -29,7 +29,7 @@ VAR CaseDefaultAssertFalse := FALSE;
(* Taken together, these help debugging, as you get more lines in the
C and the error messages reference C line numbers *)
- CONST output_line_directives = TRUE;
+ VAR output_line_directives := TRUE;
CONST output_extra_newlines = FALSE;
CONST inline_extract = FALSE;
@@ -2197,7 +2197,9 @@ BEGIN
END;
IF (*self.suppress_line_directive < 1 AND*) text_last_char = '\n' THEN
- Wr.PutText(self.c, self.line_directive);
+ IF NOT Target.ReduceTargetVariation THEN
+ Wr.PutText(self.c, self.line_directive);
+ END;
self.width := 0;
self.last_char_was_newline := TRUE;
RETURN;
@@ -2205,7 +2207,9 @@ BEGIN
IF Text.FindChar(text, '\n') # -1 THEN
self.width := 0; (* roughly *)
- Wr.PutText(self.c, self.nl_line_directive);
+ IF NOT Target.ReduceTargetVariation THEN
+ Wr.PutText(self.c, self.nl_line_directive);
+ END;
self.last_char_was_newline := TRUE;
RETURN;
END;
@@ -2217,10 +2221,12 @@ BEGIN
END;
self.width := 0;
- IF self.last_char_was_newline THEN
- Wr.PutText(self.c, self.line_directive);
- ELSE
- Wr.PutText(self.c, self.nl_line_directive);
+ IF NOT Target.ReduceTargetVariation THEN
+ IF self.last_char_was_newline THEN
+ Wr.PutText(self.c, self.line_directive);
+ ELSE
+ Wr.PutText(self.c, self.nl_line_directive);
+ END;
END;
self.last_char_was_newline := TRUE;
END print;
@@ -2339,9 +2345,10 @@ END set_error_handler;
PROCEDURE Prefix_Print(self: T; multipass: Multipass_t) =
BEGIN
self.comment("begin unit");
- self.comment("M3_TARGET = ", Target.System_name);
- (* This is an unnecessary target-specific output. *)
- (* self.comment("M3_TARGET = ", Target.System_name); *)
+ output_line_directives := output_line_directives AND NOT Target.ReduceTargetVariation;
+ IF NOT Target.ReduceTargetVariation THEN
+ self.comment("M3_TARGET = ", Target.System_name);
+ END;
self.comment("M3_WORDSIZE = ", IntToDec(Target.Word.size));
self.static_link_id := M3ID.Add("_static_link");
self.alloca_id := M3ID.Add("alloca");
@@ -4836,7 +4843,8 @@ BEGIN
& " ok2=" & BoolToText[ok2] & "\n"
);
RTIO.Flush();
- <* ASSERT FALSE *>
+ <* ASSERT ok1 *>
+ <* ASSERT ok2 *>
END;
IF type = CGType.Int32 AND TInt.EQ(i, TInt.Min32) THEN
RETURN "-" & intLiteralPrefix[type] & TInt.ToText(TInt.Max32) & intLiteralSuffix[type] & "-1";
diff --git a/m3-sys/m3cc/gcc-4.7/gcc/dbxout.c b/m3-sys/m3cc/gcc-4.7/gcc/dbxout.c
index dc52576..33f8844 100644
--- a/m3-sys/m3cc/gcc-4.7/gcc/dbxout.c
+++ b/m3-sys/m3cc/gcc-4.7/gcc/dbxout.c
@@ -1065,8 +1065,11 @@ dbxout_init (const char *input_file_name)
labels. */
ASM_GENERATE_INTERNAL_LABEL (ltext_label_name, "Ltext", 0);
- /* Put the current working directory in an N_SO symbol. */
- if (use_gnu_debug_info_extensions && !NO_DBX_MAIN_SOURCE_DIRECTORY)
+ /* Limit paths in debug output, to limit target variation. */
+ if (!reduce_target_variation)
+ {
+ /* Put the current working directory in an N_SO symbol. */
+ if (use_gnu_debug_info_extensions && !NO_DBX_MAIN_SOURCE_DIRECTORY)
{
static const char *cwd;
@@ -1087,6 +1090,7 @@ dbxout_init (const char *input_file_name)
used_ltext_label_name = true;
#endif /* no DBX_OUTPUT_MAIN_SOURCE_DIRECTORY */
}
+ }
mapped_name = remap_debug_filename (input_file_name);
#ifdef DBX_OUTPUT_MAIN_SOURCE_FILENAME
diff --git a/m3-sys/m3cc/gcc-4.7/gcc/dwarf2out.c b/m3-sys/m3cc/gcc-4.7/gcc/dwarf2out.c
index 6f60742..291aea3 100644
--- a/m3-sys/m3cc/gcc-4.7/gcc/dwarf2out.c
+++ b/m3-sys/m3cc/gcc-4.7/gcc/dwarf2out.c
@@ -15450,15 +15450,18 @@ add_gnat_descriptive_type_attribute (dw_die_ref die, tree type,
static void
add_comp_dir_attribute (dw_die_ref die)
{
+ /* Limit paths in debug output, to limit target variation. */
+ if (reduce_target_variation)
+ return;
+
const char *wd = get_src_pwd ();
- char *wd1;
if (wd == NULL)
return;
if (DWARF2_DIR_SHOULD_END_WITH_SEPARATOR)
{
- int const wdlen = (int)strlen (wd);
+ size_t const wdlen = strlen (wd);
char * const wd1 = (char *) ggc_alloc_atomic (wdlen + 2);
memcpy (wd1, wd, wdlen);
wd1 [wdlen] = DIR_SEPARATOR;
diff --git a/m3-sys/m3cc/gcc-4.7/gcc/toplev.c b/m3-sys/m3cc/gcc-4.7/gcc/toplev.c
index 63e4b92..d468632 100644
--- a/m3-sys/m3cc/gcc-4.7/gcc/toplev.c
+++ b/m3-sys/m3cc/gcc-4.7/gcc/toplev.c
@@ -217,11 +217,8 @@ const char *
get_src_pwd (void)
{
if (! src_pwd)
- {
- src_pwd = getpwd ();
- if (!src_pwd)
- src_pwd = ".";
- }
+ if (reduce_target_variation || !(src_pwd = getpwd ()))
+ src_pwd = ".";
return src_pwd;
}
diff --git a/m3-sys/m3cc/gcc-4.7/gcc/toplev.h b/m3-sys/m3cc/gcc-4.7/gcc/toplev.h
index 588cfdb..f4f7cc7 100644
--- a/m3-sys/m3cc/gcc-4.7/gcc/toplev.h
+++ b/m3-sys/m3cc/gcc-4.7/gcc/toplev.h
@@ -80,4 +80,6 @@ extern bool set_src_pwd (const char *);
extern HOST_WIDE_INT get_random_seed (bool);
extern const char *set_random_seed (const char *);
+extern bool reduce_target_variation;
+
#endif /* ! GCC_TOPLEV_H */
diff --git a/m3-sys/m3cc/gcc/gcc/m3cg/lang.opt b/m3-sys/m3cc/gcc/gcc/m3cg/lang.opt
index 3bd0469..cb0189f 100644
--- a/m3-sys/m3cc/gcc/gcc/m3cg/lang.opt
+++ b/m3-sys/m3cc/gcc/gcc/m3cg/lang.opt
@@ -28,9 +28,6 @@ m3cg
Language
M3CG
-y
-m3cg M3CG
-
fopcodes-trace
m3cg M3CG
Trace opcodes
@@ -59,10 +56,17 @@ ftypes-trace
m3cg M3CG
Trace types
+reduce-target-variation
+m3cg M3CG
+Reduce target variation somewhat, such as by omitting current working
+directory from debug info. Many necessary target variations remain.
+
v
m3cg M3CG
+print version
y
m3cg M3CG
+Trace opcodes
; This comment is to ensure we retain the blank line above.
diff --git a/m3-sys/m3cc/gcc/gcc/m3cg/parse.c b/m3-sys/m3cc/gcc/gcc/m3cg/parse.c
index 03417ed..071bbc8 100644
--- a/m3-sys/m3cc/gcc/gcc/m3cg/parse.c
+++ b/m3-sys/m3cc/gcc/gcc/m3cg/parse.c
@@ -246,6 +246,7 @@ build_case_label (tree low_value, tree high_value, tree label_decl)
/*======================================================= OPTION HANDLING ===*/
static int option_trace_all;
+bool reduce_target_variation;
/*===========================================================================*/
@@ -6364,6 +6365,10 @@ m3_handle_option (size_t code, PCSTR /*arg*/, int /*value*/)
case OPT_ftypes_trace:
option_trace_all += 1;
break;
+
+ case OPT_reduce_target_variation:
+ reduce_target_variation = true;
+ break;
}
return 1;
diff --git a/m3-sys/m3front/src/misc/Coverage.m3 b/m3-sys/m3front/src/misc/Coverage.m3
index c04c902..73fff21 100644
I don't remember what is going on here, but coverage isn't used much..
I suspect it might just have to do with forward vs. backward slashes,
and that this distinction does not really matter -- Windows accepts forward slashes
so we should just use them everywhere.
--- a/m3-sys/m3front/src/misc/Coverage.m3
+++ b/m3-sys/m3front/src/misc/Coverage.m3
@@ -77,8 +77,9 @@ PROCEDURE NoteProcedure (v: Value.T) =
PROCEDURE GenerateTables () =
VAR
nLines := MAX (0, maxLine - minLine) + 1;
+ fname := Host.FileTail (Host.filename);
l_header := TLen (Header);
- l_fname := TLen (Host.filename);
+ l_fname := TLen (fname);
l_trailer := TLen (Trailer);
size : INTEGER;
p : ProcHead;
@@ -124,10 +125,10 @@ PROCEDURE GenerateTables () =
(* CG.Init_int (size, Target.Integer.size, TInt.Zero, FALSE); *)
INC (size, Target.Integer.size); (*timestamp*)
- CG.Init_intt (size, Target.Integer.size, Text.Length (Host.filename), FALSE);
+ CG.Init_intt (size, Target.Integer.size, Text.Length (fname), FALSE);
INC (size, Target.Integer.size); (*fileLen*)
- CG.Init_chars (size, Host.filename, FALSE);
+ CG.Init_chars (size, fname, FALSE);
INC (size, l_fname * Target.Char.size); (*file*)
CG.Init_intt (size, Target.Integer.size, minLine, FALSE);
diff --git a/m3-sys/m3front/src/misc/Host.i3 b/m3-sys/m3front/src/misc/Host.i3
index c71489f..af6a89a 100644
--- a/m3-sys/m3front/src/misc/Host.i3
+++ b/m3-sys/m3front/src/misc/Host.i3
@@ -75,4 +75,7 @@ PROCEDURE OpenUnit (name: M3ID.T; interface, generic: BOOLEAN;
PROCEDURE CloseFile (rd: File.T);
+PROCEDURE FileTail (path: TEXT): TEXT;
+ (* returns the 'tail' of 'path' -- after any slashes or even spaces *)
+
END Host.
diff --git a/m3-sys/m3front/src/misc/Host.m3 b/m3-sys/m3front/src/misc/Host.m3
index 962d3c6..79042e3 100644
--- a/m3-sys/m3front/src/misc/Host.m3
+++ b/m3-sys/m3front/src/misc/Host.m3
@@ -9,7 +9,7 @@
MODULE Host;
-IMPORT File, Text, (*ETimer, M3Timers,*) M3ID, M3Compiler;
+IMPORT File, Text, (*ETimer, M3Timers,*) M3ID, M3Compiler, Target;
PROCEDURE Initialize (READONLY options: ARRAY OF TEXT): BOOLEAN =
BEGIN
@@ -192,5 +192,24 @@ PROCEDURE CloseFile (rd: File.T) =
END;
END CloseFile;
+PROCEDURE FileTail (path: TEXT): TEXT =
+ VAR c: CHAR;
+ BEGIN
+ IF NOT Target.ReduceTargetVariation THEN RETURN path; END;
+
+ IF (path = NIL) THEN RETURN NIL END;
+
+ (* search for the last slash or blank in the string *)
+ FOR x := Text.Length (path) - 1 TO 0 BY -1 DO
+ c := Text.GetChar (path, x);
+ IF (c = '/') OR (c = ' ') OR (c = '\\') THEN
+ RETURN Text.Sub (path, x+1);
+ END;
+ END;
+
+ (* no slashes *)
+ RETURN path;
+ END FileTail;
+
BEGIN
END Host.
diff --git a/m3-sys/m3front/src/misc/M3Header.m3 b/m3-sys/m3front/src/misc/M3Header.m3
index 1e4decf..877d77c 100644
--- a/m3-sys/m3front/src/misc/M3Header.m3
+++ b/m3-sys/m3front/src/misc/M3Header.m3
@@ -104,7 +104,7 @@ PROCEDURE PushGeneric (VAR s: State) =
IF (s.generic = NIL) THEN s.failed := TRUE; RETURN; END;
(* build a synthetic file name & start reading *)
- filename := old_filename & " => " & filename;
+ filename := Host.FileTail(old_filename) & " => " & filename;
Scanner.Push (filename, s.generic, is_main := Scanner.in_main);
(* make sure we got what we wanted *)
diff --git a/m3-sys/m3front/src/misc/Scanner.m3 b/m3-sys/m3front/src/misc/Scanner.m3
index 7470374..e1dc024 100644
--- a/m3-sys/m3front/src/misc/Scanner.m3
+++ b/m3-sys/m3front/src/misc/Scanner.m3
@@ -228,13 +228,16 @@ PROCEDURE Here (VAR file: TEXT; VAR line: INTEGER) =
BEGIN
file := files [offset DIV MaxLines];
line := offset MOD MaxLines;
+ IF Target.ReduceTargetVariation THEN
+ file := Host.FileTail(file);
+ END;
END Here;
PROCEDURE LocalHere (VAR file: TEXT; VAR line: INTEGER) =
VAR fnum := offset DIV MaxLines;
BEGIN
IF (local_files[fnum] = NIL) THEN
- local_files[fnum] := files[fnum];
+ local_files[fnum] := Host.FileTail(files[fnum]);
END;
file := local_files [fnum];
line := offset MOD MaxLines;
diff --git a/m3-sys/m3front/src/values/Module.m3 b/m3-sys/m3front/src/values/Module.m3
index a085eab..576c857 100644
--- a/m3-sys/m3front/src/values/Module.m3
+++ b/m3-sys/m3front/src/values/Module.m3
@@ -421,7 +421,7 @@ PROCEDURE PushGeneric (t: T; VAR rd: File.T): M3ID.T =
END;
(* build a synthetic file name & start reading *)
- filename := old_filename & " => " & filename;
+ filename := Host.FileTail(old_filename) & " => " & filename;
Scanner.Push (filename, rd, is_main := Scanner.in_main);
t.genericFile := filename;
diff --git a/m3-sys/m3middle/src/Target.i3 b/m3-sys/m3middle/src/Target.i3
index fe198d2..cd7acee 100644
--- a/m3-sys/m3middle/src/Target.i3
+++ b/m3-sys/m3middle/src/Target.i3
@@ -529,4 +529,8 @@ VAR (*CONST*)
test for nested procedures passed as parameters must be more
elaborate (e.g. HPPA). *)
+ (* This removes some unnecessary target variation in the output,
+ * such as current working directory in debug output. *)
+ ReduceTargetVariation: BOOLEAN;
+
END Target.
diff --git a/scripts/python/pylib.py b/scripts/python/pylib.py
index 73e622f..487ad17 100755
--- a/scripts/python/pylib.py
+++ b/scripts/python/pylib.py
@@ -388,7 +388,7 @@ def _GetAllTargets():
_CBackend = "c" in sys.argv or "C" in sys.argv
_BuildDirC = ["", "c"][_CBackend]
-_PossibleCm3Flags = ["boot", "keep", "override", "commands", "verbose", "why"]
+_PossibleCm3Flags = ["boot", "keep", "override", "commands", "verbose", "why", "reduce-target-variation", "reducetargetvariation"]
_SkipGccFlags = ["nogcc", "skipgcc", "omitgcc"]
_PossiblePylibFlags = ["noclean", "nocleangcc", "c", "C"] + _SkipGccFlags + _PossibleCm3Flags
@@ -1610,9 +1610,9 @@ def Boot():
Makefile.close()
if vms or nt:
- _MakeZip(BootDir[2:])
+ pass#_MakeZip(BootDir[2:])
else:
- _MakeTGZ(BootDir[2:])
+ pass#_MakeTGZ(BootDir[2:])
#-----------------------------------------------------------------------------
# map action names to code and possibly other data
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://m3lists.elegosoft.com/pipermail/m3devel/attachments/20170704/1aacffa8/attachment-0001.html>
More information about the M3devel
mailing list