grub-devel.gnu.org archive mirror
 help / color / mirror / Atom feed
From: "Vladimir 'φ-coder/phcoder' Serbinenko" <phcoder@gmail.com>
To: grub-devel@gnu.org
Subject: Re: [PATCH] Re-enable grub-extras
Date: Thu, 23 Sep 2010 16:36:33 +0200	[thread overview]
Message-ID: <4C9B65F1.10909@gmail.com> (raw)
In-Reply-To: <20100923130819.GV21862@riva.ucam.org>

[-- Attachment #1: Type: text/plain, Size: 19637 bytes --]

Go ahead for both trunk and extras
On 09/23/2010 03:08 PM, Colin Watson wrote:
> On Tue, Sep 21, 2010 at 07:35:30PM +0100, Colin Watson wrote:
>   
>> Here's a patch to re-enable grub-extras by allowing extra modules to
>> provide Autogen definitions files.  A few things to note:
>>     
> Vladimir commented on this on IRC, noting three problems.  In reverse
> order of complexity:
>
> 1) The GRUB_CONTRIB check in autogen.sh should also check for
>    grub-core/contrib.
>
> Fixed.
>
> 2) What's the problem with renaming g2hdr.bin to g2hdr.img?
>
> I know of at least one piece of software which expects the current file
> name: win32-loader, written by Robert and maintained in Debian.  It
> probably wouldn't be completely awful to change this, but it would be
> nice not to need to.  That said, perhaps g2hdr.bin could be a symlink to
> g2hdr.img (which could be done in Makefile.common).  I've updated my
> patch to do this, and removed the extension-override facility from
> gentpl.py.
>
> 3) It would be nice if extra modules could add new sources to existing
>    targets defined in one of the main definitions files: for example, it
>    would be useful to be able to add zfs to libgrub.a.
>
> This was the hard bit that took me a day or two to fix.
>
> Vladimir's original suggestion on IRC was to create a separate
> libgrubzfs.a and arrange to link the utilities with that as well.  When
> I looked into this, I decided that I didn't like this approach because
> it simply pushes the problem of appending to an existing list up a
> level; as far as the Autogen definitions go, there isn't a significant
> difference between appending to grub_probe_LDADD and appending to
> libgrub_a_SOURCES.  I would prefer to solve this some other way.
>
> My preferred approach is to try not to expose any of the difficulties in
> the definitions file, so that it can just look like this:
>
>   library = {
>     name = libgrub.a;
>     common = contrib/zfs/zfs.c;
>     common = contrib/zfs/zfs_lzjb.c;
>     common = contrib/zfs/zfs_sha256.c;
>     common = contrib/zfs/zfs_fletcher.c;
>     cppflags = '$(ZFS_CPPFLAGS)';
>     cflags = '$(ZFS_CFLAGS)';
>   };
>
> Doing this actually turned out to be very difficult because Autogen's
> native macros aren't powerful enough: they can only really emit text
> rather than using temporary storage, which means that you can't do
> things like "look for all the library blocks with the same name and
> concatenate them" or "append to _SOURCES variables if we've seen them
> before".  Some Automake variables can be handled with simple appends
> while some of them have some boilerplate only at the start which
> shouldn't be repeated; Automake variables must be initialised exactly
> once on all possible intersections of conditional paths; and we should
> also avoid generating duplicate rules.  All these constraints make it a
> hard problem.
>
> Fortunately, Autogen supports embedding bits of Guile, which makes the
> problem merely hard rather than impossible.  I tried a few different
> approaches, and the one that I found to be most practical is in the
> following patch: effectively we remember whether we've seen a given
> target before and treat it slightly differently, and furthermore we emit
> initialisations for non-global variables to avoid problems with
> Automake's restrictions on variable initialisation.
>
> For now, I only implemented this for 'library' blocks, so something that
> wants to compile extra files into the GRUB kernel for example will be
> out of luck.  If people feel this is an onerous restriction then it's
> fairly easy to fix; I was just trying not to change too much at once,
> particularly for paths I wasn't going to test.
>
> Doing this involved feeding all the files relevant to a given Automake
> input stream to Autogen in one go, so I eliminated Makefile.gcry.am.
>
> This was an exercise in programming in six languages at once (Python,
> Autogen native macros, Guile, Autogen definitions, Automake, and make).
> I found it to be a bit much, and my impression is that Autogen got in
> the way more than it helped.  I suggest that after 1.99 we should look
> at having a Python program generate Automake input directly, which would
> avoid many contortions and allow us to improve the output (it could
> merge the multiple _SOURCES lines into just one per conditional path,
> for instance).  The .def file format is of course just fine as it is,
> and could easily be preserved.
>
>   
>>   * Extra modules may also provide Makefile.common with literal Automake
>>     input, for those cases where it's too hard to cram things into
>>     Autogen.
>>     
> I also added Makefile.core.common and Makefile.util.common (starting to
> run out of good names) for literal Automake input that should only be
> used in grub-core or at the top level respectively.  ntldr-img needed
> the former.
>
> Once again, the required patches to grub-extras modules are attached.
>
> 2010-09-22  Colin Watson  <cjwatson@ubuntu.com>
>
> 	Re-enable grub-extras.
>
> 	* autogen.sh: Create symlinks to ${GRUB_CONTRIB} if necessary to
> 	avoid confusing Automake.  Run autogen only twice, once for the top
> 	level and once for grub-core.  Add Makefile.util.def and
> 	Makefile.core.def from extra modules to the appropriate autogen
> 	invocations.  If Makefile.common exists in an extra module, include
> 	it in both Makefile.util.am and grub-core/Makefile.core.am;
> 	similarly, include any Makefile.util.common file in Makefile.util.am
> 	and any Makefile.core.common file in grub-core/Makefile.core.am.
> 	* conf/Makefile.common ($(top_srcdir)/grub-core/Makefile.core.am):
> 	Depend on $(top_srcdir)/grub-core/Makefile.gcry.def.
> 	($(top_srcdir)/grub-core/Makefile.gcry.def): Remove.
> 	* grub-core/Makefile.am: Remove inclusion of Makefile.gcry.am.
>
> 	* gentpl.py (gvar_add): Turn GVARS into a set.
> 	(global_variable_initializers): Sort global variables on output.
> 	(vars_init): New function.
> 	(first_time): Likewise.
> 	(library): Ensure that non-global variable initialisations are
> 	emitted before the first time we emit code for a library block.
> 	Append to variables rather than setting them.  Only emit
> 	noinst_LIBRARIES, BUILT_SOURCES, and CLEANFILES the first time for
> 	each conditional path.
> 	(program): installdir() emits an Autogen macro, so must be passed to
> 	var_add rather than gvar_add.
> 	(data): Likewise.
> 	(script): Likewise.
> 	(rules): New function, centralising handling for different target
> 	types.  Set up Guile association lists for first_time and vars_init,
> 	and send most output to a diversion so that variable initialisations
> 	can be emitted first.
> 	(module_rules): Use new rules function.
> 	(kernel_rules): Likewise.
> 	(image_rules): Likewise.
> 	(library_rules): Likewise.
> 	(program_rules): Likewise.
> 	(script_rules): Likewise.
> 	(data_rules): Likewise.
>
> 	* configure.ac: Add AC_PROG_LN_S, for the benefit of ntldr-img.
>
> 	* .bzrignore: Add contrib and grub-core/contrib.  Remove
> 	grub-core/Makefile.gcry.am.
>
> === modified file '.bzrignore'
> --- .bzrignore	2010-09-20 13:03:47 +0000
> +++ .bzrignore	2010-09-22 13:07:15 +0000
> @@ -104,9 +104,10 @@ grub-core/lib/libgcrypt-grub
>  **/.deps-core
>  **/.dirstamp
>  Makefile.util.am
> +contrib
>  grub-core/Makefile.core.am
> -grub-core/Makefile.gcry.am
>  grub-core/Makefile.gcry.def
> +grub-core/contrib
>  grub-core/genmod.sh
>  grub-core/gensyminfo.sh
>  grub-core/*.module
>
> === modified file 'autogen.sh'
> --- autogen.sh	2010-08-23 08:37:29 +0000
> +++ autogen.sh	2010-09-23 10:36:52 +0000
> @@ -14,9 +14,50 @@ echo "Creating Makefile.tpl..."
>  python gentpl.py | sed -e '/^$/{N;/^\n$/D;}' > Makefile.tpl
>  
>  echo "Running autogen..."
> -autogen -T Makefile.tpl Makefile.util.def | sed -e '/^$/{N;/^\n$/D;}' > Makefile.util.am
> -autogen -T Makefile.tpl grub-core/Makefile.core.def | sed -e '/^$/{N;/^\n$/D;}' > grub-core/Makefile.core.am
> -autogen -T Makefile.tpl grub-core/Makefile.gcry.def | sed -e '/^$/{N;/^\n$/D;}' > grub-core/Makefile.gcry.am
> +
> +# Automake doesn't like including files from a path outside the project.
> +rm -f contrib grub-core/contrib
> +if [ "x${GRUB_CONTRIB}" != x ]; then
> +  [ "${GRUB_CONTRIB}" = contrib ] || ln -s "${GRUB_CONTRIB}" contrib
> +  [ "${GRUB_CONTRIB}" = grub-core/contrib ] || ln -s ../contrib grub-core/contrib
> +fi
> +
> +UTIL_DEFS=Makefile.util.def
> +CORE_DEFS='grub-core/Makefile.core.def grub-core/Makefile.gcry.def'
> +
> +for extra in contrib/*/Makefile.util.def; do
> +  if test -e "$extra"; then
> +    UTIL_DEFS="$UTIL_DEFS $extra"
> +  fi
> +done
> +
> +for extra in contrib/*/Makefile.core.def; do
> +  if test -e "$extra"; then
> +    CORE_DEFS="$CORE_DEFS $extra"
> +  fi
> +done
> +
> +cat $UTIL_DEFS | autogen -T Makefile.tpl | sed -e '/^$/{N;/^\n$/D;}' > Makefile.util.am
> +cat $CORE_DEFS | autogen -T Makefile.tpl | sed -e '/^$/{N;/^\n$/D;}' > grub-core/Makefile.core.am
> +
> +for extra in contrib/*/Makefile.common; do
> +  if test -e "$extra"; then
> +    echo "include $extra" >> Makefile.util.am
> +    echo "include $extra" >> grub-core/Makefile.core.am
> +  fi
> +done
> +
> +for extra in contrib/*/Makefile.util.common; do
> +  if test -e "$extra"; then
> +    echo "include $extra" >> Makefile.util.am
> +  fi
> +done
> +
> +for extra in contrib/*/Makefile.core.common; do
> +  if test -e "$extra"; then
> +    echo "include $extra" >> grub-core/Makefile.core.am
> +  fi
> +done
>  
>  echo "Saving timestamps..."
>  echo timestamp > stamp-h.in
>
> === modified file 'conf/Makefile.common'
> --- conf/Makefile.common	2010-09-21 12:37:50 +0000
> +++ conf/Makefile.common	2010-09-22 13:09:44 +0000
> @@ -146,11 +146,7 @@ $(top_srcdir)/Makefile.util.am: $(top_sr
>  	mv $@.new $@
>  
>  .PRECIOUS: $(top_srcdir)/grub-core/Makefile.core.am
> -$(top_srcdir)/grub-core/Makefile.core.am: $(top_srcdir)/grub-core/Makefile.core.def $(top_srcdir)/Makefile.tpl
> -	autogen -T $(top_srcdir)/Makefile.tpl $< | sed -e '/^$$/{N;/^\\n$$/D;}' > $@.new || (rm -f $@.new; exit 1)
> -	mv $@.new $@
> -
> -.PRECIOUS: $(top_srcdir)/grub-core/Makefile.gcry.am
> -$(top_srcdir)/grub-core/Makefile.gcry.am: $(top_srcdir)/grub-core/Makefile.gcry.def $(top_srcdir)/Makefile.tpl
> -	autogen -T $(top_srcdir)/Makefile.tpl $< | sed -e '/^$$/{N;/^\\n$$/D;}' > $@.new || (rm -f $@.new; exit 1)
> +$(top_srcdir)/grub-core/Makefile.core.am: $(top_srcdir)/grub-core/Makefile.core.def $(top_srcdir)/grub-core/Makefile.gcry.def $(top_srcdir)/Makefile.tpl
> +	if [ "x$$GRUB_CONTRIB" != x ]; then echo "You need to run ./autogen.sh manually." >&2; exit 1; fi
> +	autogen -T $(top_srcdir)/Makefile.tpl $(top_srcdir)/grub-core/Makefile.core.def $(top_srcdir)/grub-core/Makefile.gcry.def | sed -e '/^$$/{N;/^\\n$$/D;}' > $@.new || (rm -f $@.new; exit 1)
>  	mv $@.new $@
>
> === modified file 'configure.ac'
> --- configure.ac	2010-09-21 09:42:30 +0000
> +++ configure.ac	2010-09-22 12:11:35 +0000
> @@ -235,6 +235,7 @@ AC_PROG_LEX
>  AC_PROG_YACC
>  AC_PROG_MAKE_SET
>  AC_PROG_MKDIR_P
> +AC_PROG_LN_S
>  
>  if test "x$LEX" = "x:"; then
>    AC_MSG_ERROR([flex is not found])
>
> === modified file 'gentpl.py'
> --- gentpl.py	2010-09-19 13:24:45 +0000
> +++ gentpl.py	2010-09-22 21:01:22 +0000
> @@ -70,16 +70,15 @@ for platform in GRUB_PLATFORMS:
>  #
>  # Global variables
>  #
> -GVARS = []
> +GVARS = set()
>  
>  def gvar_add(var, value):
> -    if var not in GVARS:
> -        GVARS.append(var)
> +    GVARS.add(var)
>      return var + " += " + value + "\n"
>  
>  def global_variable_initializers():
>      r = ""
> -    for var in GVARS:
> +    for var in sorted(GVARS):
>          r += var + " ?= \n"
>      return r
>  
> @@ -87,6 +86,16 @@ def global_variable_initializers():
>  # Per PROGRAM/SCRIPT variables 
>  #
>  
> +def vars_init(*var_list):
> +    r = "[+ IF (if (not (assoc-ref seen-vars (get \".name\"))) \"seen\") +]"
> +    r += "[+ (out-suspend \"v\") +]"
> +    for var in var_list:
> +        r += var + "  = \n"
> +    r += "[+ (out-resume \"v\") +]"
> +    r += "[+ (set! seen-vars (assoc-set! seen-vars (get \".name\") 0)) +]"
> +    r += "[+ ENDIF +]"
> +    return first_time(r)
> +
>  def var_set(var, value):
>      return var + "  = " + value + "\n"
>  
> @@ -257,6 +266,15 @@ def platform_ccasflags(p): return platfo
>  def platform_stripflags(p): return platform_specific_values(p, "_stripflags", "stripflags")
>  def platform_objcopyflags(p): return platform_specific_values(p, "_objcopyflags", "objcopyflags")
>  
> +#
> +# Emit snippet only the first time through for the current name.
> +#
> +def first_time(snippet):
> +    r = "[+ IF (if (not (assoc-ref seen-target (get \".name\"))) \"seen\") +]"
> +    r += snippet
> +    r += "[+ ENDIF +]"
> +    return r
> +
>  def module(platform):
>      r = set_canonical_name_suffix(".module")
>  
> @@ -341,18 +359,25 @@ fi
>  
>  def library(platform):
>      r = set_canonical_name_suffix("")
> -    r += gvar_add("noinst_LIBRARIES", "[+ name +]")
> -    r += var_set(cname() + "_SOURCES", platform_sources(platform))
> -    r += var_set("nodist_" + cname() + "_SOURCES", platform_nodist_sources(platform))
> -    r += var_set(cname() + "_CFLAGS", "$(AM_CFLAGS) $(CFLAGS_LIBRARY) " + platform_cflags(platform))
> -    r += var_set(cname() + "_CPPFLAGS", "$(AM_CPPFLAGS) $(CPPFLAGS_LIBRARY) " + platform_cppflags(platform))
> -    r += var_set(cname() + "_CCASFLAGS", "$(AM_CCASFLAGS) $(CCASFLAGS_LIBRARY) " + platform_ccasflags(platform))
> -    # r += var_set(cname() + "_DEPENDENCIES", platform_dependencies(platform) + " " + platform_ldadd(platform))
>  
> -    r += gvar_add("EXTRA_DIST", extra_dist())
> -    r += gvar_add("BUILT_SOURCES", "$(nodist_" + cname() + "_SOURCES)")
> -    r += gvar_add("CLEANFILES", "$(nodist_" + cname() + "_SOURCES)")
> +    r += vars_init(cname() + "_SOURCES",
> +                   "nodist_" + cname() + "_SOURCES",
> +                   cname() + "_CFLAGS",
> +                   cname() + "_CPPFLAGS",
> +                   cname() + "_CCASFLAGS")
> +    #              cname() + "_DEPENDENCIES")
>  
> +    r += first_time(gvar_add("noinst_LIBRARIES", "[+ name +]"))
> +    r += var_add(cname() + "_SOURCES", platform_sources(platform))
> +    r += var_add("nodist_" + cname() + "_SOURCES", platform_nodist_sources(platform))
> +    r += var_add(cname() + "_CFLAGS", first_time("$(AM_CFLAGS) $(CFLAGS_LIBRARY) ") + platform_cflags(platform))
> +    r += var_add(cname() + "_CPPFLAGS", first_time("$(AM_CPPFLAGS) $(CPPFLAGS_LIBRARY) ") + platform_cppflags(platform))
> +    r += var_add(cname() + "_CCASFLAGS", first_time("$(AM_CCASFLAGS) $(CCASFLAGS_LIBRARY) ") + platform_ccasflags(platform))
> +    # r += var_add(cname() + "_DEPENDENCIES", platform_dependencies(platform) + " " + platform_ldadd(platform))
> +
> +    r += gvar_add("EXTRA_DIST", extra_dist())
> +    r += first_time(gvar_add("BUILT_SOURCES", "$(nodist_" + cname() + "_SOURCES)"))
> +    r += first_time(gvar_add("CLEANFILES", "$(nodist_" + cname() + "_SOURCES)"))
>      return r
>  
>  def installdir(default="bin"):
> @@ -376,7 +401,7 @@ def program(platform, test=False):
>      r += gvar_add("check_PROGRAMS", "[+ name +]")
>      r += gvar_add("TESTS", "[+ name +]")
>      r += "[+ ELSE +]"
> -    r += gvar_add(installdir() + "_PROGRAMS", "[+ name +]")
> +    r += var_add(installdir() + "_PROGRAMS", "[+ name +]")
>      r += "[+ IF mansection +]" + manpage() + "[+ ENDIF +]"
>      r += "[+ ENDIF +]"
>  
> @@ -397,7 +422,7 @@ def program(platform, test=False):
>  def data(platform):
>      r  = gvar_add("EXTRA_DIST", platform_sources(platform))
>      r += gvar_add("EXTRA_DIST", extra_dist())
> -    r += gvar_add(installdir() + "_DATA", platform_sources(platform))
> +    r += var_add(installdir() + "_DATA", platform_sources(platform))
>      return r
>  
>  def script(platform):
> @@ -405,7 +430,7 @@ def script(platform):
>      r += gvar_add("check_SCRIPTS", "[+ name +]")
>      r += gvar_add ("TESTS", "[+ name +]")
>      r += "[+ ELSE +]"
> -    r += gvar_add(installdir() + "_SCRIPTS", "[+ name +]")
> +    r += var_add(installdir() + "_SCRIPTS", "[+ name +]")
>      r += "[+ IF mansection +]" + manpage() + "[+ ENDIF +]"
>      r += "[+ ENDIF +]"
>  
> @@ -418,33 +443,43 @@ chmod a+x [+ name +]
>      r += gvar_add("dist_noinst_DATA", platform_sources(platform))
>      return r
>  
> +def rules(target, closure):
> +    # Create association lists for the benefit of first_time and vars_init.
> +    r = "[+ (define seen-target '()) +]"
> +    r += "[+ (define seen-vars '()) +]"
> +    # Most output goes to a diversion.  This allows us to emit variable
> +    # initializations before everything else.
> +    r += "[+ (out-push-new) +]"
> +
> +    r += "[+ FOR " + target + " +]"
> +    r += foreach_enabled_platform(
> +        lambda p: under_platform_specific_conditionals(p, closure(p)))
> +    # Remember that we've seen this target.
> +    r += "[+ (set! seen-target (assoc-set! seen-target (get \".name\") 0)) +]"
> +    r += "[+ ENDFOR +]"
> +    r += "[+ (out-pop #t) +]"
> +    return r
> +
>  def module_rules():
> -    return "[+ FOR module +]" + foreach_enabled_platform(
> -        lambda p: under_platform_specific_conditionals(p, module(p))) + "[+ ENDFOR +]"
> +    return rules("module", module)
>  
>  def kernel_rules():
> -    return "[+ FOR kernel +]" + foreach_enabled_platform(
> -        lambda p: under_platform_specific_conditionals(p, kernel(p))) + "[+ ENDFOR +]"
> +    return rules("kernel", kernel)
>  
>  def image_rules():
> -    return "[+ FOR image +]" + foreach_enabled_platform(
> -        lambda p: under_platform_specific_conditionals(p, image(p))) + "[+ ENDFOR +]"
> +    return rules("image", image)
>  
>  def library_rules():
> -    return "[+ FOR library +]" + foreach_enabled_platform(
> -        lambda p: under_platform_specific_conditionals(p, library(p))) + "[+ ENDFOR +]"
> +    return rules("library", library)
>  
>  def program_rules():
> -    return "[+ FOR program +]" + foreach_enabled_platform(
> -        lambda p: under_platform_specific_conditionals(p, program(p))) + "[+ ENDFOR +]"
> +    return rules("program", program)
>  
>  def script_rules():
> -    return "[+ FOR script +]" + foreach_enabled_platform(
> -        lambda p: under_platform_specific_conditionals(p, script(p))) + "[+ ENDFOR +]"
> +    return rules("script", script)
>  
>  def data_rules():
> -    return "[+ FOR data +]" + foreach_enabled_platform(
> -        lambda p: under_platform_specific_conditionals(p, data(p))) + "[+ ENDFOR +]"
> +    return rules("data", data)
>  
>  print "[+ AutoGen5 template +]\n"
>  a = module_rules()
>
> === modified file 'grub-core/Makefile.am'
> --- grub-core/Makefile.am	2010-09-19 20:22:43 +0000
> +++ grub-core/Makefile.am	2010-09-22 13:11:11 +0000
> @@ -51,7 +51,6 @@ grub_script.yy.c: grub_script.yy.h
>  CLEANFILES += grub_script.yy.c grub_script.yy.h
>  
>  include $(srcdir)/Makefile.core.am
> -include $(srcdir)/Makefile.gcry.am
>  
>  KERNEL_HEADER_FILES += $(top_srcdir)/include/grub/cache.h
>  KERNEL_HEADER_FILES += $(top_srcdir)/include/grub/command.h
>
>   


-- 
Regards
Vladimir 'φ-coder/phcoder' Serbinenko



[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 294 bytes --]

  parent reply	other threads:[~2010-09-23 16:06 UTC|newest]

Thread overview: 11+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2010-09-21 18:35 [PATCH] Re-enable grub-extras Colin Watson
2010-09-23 13:08 ` Colin Watson
2010-09-23 13:12   ` Colin Watson
2010-09-23 14:23     ` Colin Watson
2010-09-23 14:36   ` Vladimir 'φ-coder/phcoder' Serbinenko [this message]
2010-09-24  9:09     ` Colin Watson
2010-09-23 20:27   ` [PATCH] Trunk: usbtrans.c - wrong max packet size for bulk transfer Aleš Nesrsta
2010-10-01  7:35     ` Vladimir 'φ-coder/phcoder' Serbinenko
2010-10-01 15:07       ` Aleš Nesrsta
2010-10-01 18:33         ` Vladimir 'φ-coder/phcoder' Serbinenko
2010-10-02 18:58           ` Aleš Nesrsta

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=4C9B65F1.10909@gmail.com \
    --to=phcoder@gmail.com \
    --cc=grub-devel@gnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).